rails-ai-context 5.30.2 → 5.32.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 +868 -0
- data/CONTEXT.md +7 -3
- data/README.md +1 -1
- data/docs/ARCHITECTURE.md +3 -3
- data/docs/CLI.md +1 -1
- data/docs/COMPATIBILITY.md +50 -14
- data/docs/CONFIGURATION.md +4 -1
- data/docs/FAQ.md +1 -1
- data/docs/GUIDE.md +14 -8
- data/docs/INTROSPECTORS.md +50 -23
- data/docs/SECURITY.md +56 -1
- data/docs/TOOLS.md +58 -12
- data/lib/generators/rails_ai_context/install/install_generator.rb +1 -0
- data/lib/rails_ai_context/action_filters.rb +121 -48
- data/lib/rails_ai_context/app_commands.rb +2 -2
- data/lib/rails_ai_context/app_kind.rb +65 -8
- data/lib/rails_ai_context/ast_cache.rb +53 -18
- data/lib/rails_ai_context/base_mixins.rb +46 -16
- data/lib/rails_ai_context/boot_manager.rb +5 -1
- data/lib/rails_ai_context/cli/entry_boot.rb +17 -2
- data/lib/rails_ai_context/cli/tool_runner.rb +15 -1
- data/lib/rails_ai_context/concern_macros.rb +791 -111
- data/lib/rails_ai_context/concern_membership.rb +25 -5
- data/lib/rails_ai_context/concern_paths.rb +63 -17
- data/lib/rails_ai_context/config_yaml.rb +40 -0
- data/lib/rails_ai_context/configuration.rb +3 -3
- data/lib/rails_ai_context/data/docs/index.json +324 -0
- data/lib/rails_ai_context/database_yml.rb +136 -0
- data/lib/rails_ai_context/doctor.rb +30 -37
- data/lib/rails_ai_context/erb_source.rb +40 -0
- data/lib/rails_ai_context/fingerprinter.rb +48 -22
- data/lib/rails_ai_context/fixture_keys.rb +42 -1
- data/lib/rails_ai_context/gem_lock.rb +275 -47
- data/lib/rails_ai_context/git_ignore.rb +13 -11
- data/lib/rails_ai_context/hydrators/hydration_formatter.rb +8 -2
- data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +10 -5
- data/lib/rails_ai_context/introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +27 -19
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -8
- data/lib/rails_ai_context/introspectors/action_resolver.rb +38 -26
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/active_record_settings.rb +56 -0
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +98 -22
- data/lib/rails_ai_context/introspectors/admin_resources.rb +101 -0
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +64 -0
- data/lib/rails_ai_context/introspectors/apartment_config.rb +47 -0
- data/lib/rails_ai_context/introspectors/api_introspector.rb +125 -26
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +24 -19
- data/lib/rails_ai_context/introspectors/ast_walk.rb +18 -0
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +71 -18
- data/lib/rails_ai_context/introspectors/autoload_introspector.rb +62 -15
- data/lib/rails_ai_context/introspectors/base.rb +38 -0
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +349 -39
- data/lib/rails_ai_context/introspectors/component_introspector.rb +26 -14
- data/lib/rails_ai_context/introspectors/config_introspector.rb +22 -9
- data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +1 -4
- data/lib/rails_ai_context/introspectors/controller_filters.rb +335 -31
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +315 -129
- data/lib/rails_ai_context/introspectors/controller_settings.rb +182 -0
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +18 -15
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/declared_constant.rb +108 -23
- data/lib/rails_ai_context/introspectors/eager_load.rb +12 -7
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +13 -5
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +62 -17
- data/lib/rails_ai_context/introspectors/env_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/env_references.rb +21 -4
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +37 -22
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/gemfile_gems.rb +53 -5
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +109 -0
- data/lib/rails_ai_context/introspectors/helper_names.rb +12 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +101 -46
- data/lib/rails_ai_context/introspectors/includers.rb +6 -1
- data/lib/rails_ai_context/introspectors/initializer_introspector.rb +2 -5
- data/lib/rails_ai_context/introspectors/job_introspector.rb +548 -103
- data/lib/rails_ai_context/introspectors/listener_registration.rb +11 -1
- data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +51 -4
- data/lib/rails_ai_context/introspectors/listeners/autoload_ignore_listener.rb +31 -0
- data/lib/rails_ai_context/introspectors/listeners/autoload_paths_listener.rb +4 -47
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +80 -14
- data/lib/rails_ai_context/introspectors/listeners/branch_conditions.rb +156 -0
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +85 -23
- data/lib/rails_ai_context/introspectors/listeners/class_definition_listener.rb +35 -6
- data/lib/rails_ai_context/introspectors/listeners/component_structure_listener.rb +30 -2
- data/lib/rails_ai_context/introspectors/listeners/conditional_macro_listener.rb +19 -0
- data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +255 -10
- data/lib/rails_ai_context/introspectors/listeners/constant_reference_listener.rb +34 -0
- data/lib/rails_ai_context/introspectors/listeners/constructor_macro_listener.rb +123 -0
- data/lib/rails_ai_context/introspectors/listeners/definition_file_paths_listener.rb +66 -0
- data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +31 -0
- data/lib/rails_ai_context/introspectors/listeners/filter_macro_listener.rb +54 -0
- data/lib/rails_ai_context/introspectors/listeners/fixture_paths_listener.rb +62 -0
- data/lib/rails_ai_context/introspectors/listeners/gemfile_dsl_listener.rb +42 -1
- data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +28 -2
- data/lib/rails_ai_context/introspectors/listeners/grape_api_listener.rb +137 -0
- data/lib/rails_ai_context/introspectors/listeners/http_client_call_listener.rb +50 -0
- data/lib/rails_ai_context/introspectors/listeners/i18n_load_path_listener.rb +54 -0
- data/lib/rails_ai_context/introspectors/listeners/literal_paths.rb +123 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +248 -4
- data/lib/rails_ai_context/introspectors/listeners/mailbox_routing_listener.rb +2 -1
- data/lib/rails_ai_context/introspectors/listeners/method_call_listener.rb +7 -1
- data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +352 -69
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +1 -1
- data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +35 -26
- data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +62 -29
- data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +8 -0
- data/lib/rails_ai_context/introspectors/listeners/namespaced_roots_listener.rb +35 -0
- data/lib/rails_ai_context/introspectors/listeners/nested_constants_listener.rb +32 -0
- data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +67 -0
- data/lib/rails_ai_context/introspectors/listeners/preview_paths_listener.rb +16 -7
- data/lib/rails_ai_context/introspectors/listeners/proc_literal_listener.rb +42 -0
- data/lib/rails_ai_context/introspectors/listeners/queue_assignment_listener.rb +54 -0
- data/lib/rails_ai_context/introspectors/listeners/rake_task_dsl_listener.rb +21 -8
- data/lib/rails_ai_context/introspectors/listeners/route_files_listener.rb +4 -11
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +461 -65
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +76 -14
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +40 -1
- data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/view_paths_listener.rb +37 -0
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +47 -3
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +4 -3
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +72 -12
- data/lib/rails_ai_context/introspectors/migration_replay.rb +110 -14
- data/lib/rails_ai_context/introspectors/model_introspector.rb +708 -214
- data/lib/rails_ai_context/introspectors/module_aliases.rb +11 -3
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +9 -57
- data/lib/rails_ai_context/introspectors/observability_introspector.rb +5 -8
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +28 -8
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +118 -51
- data/lib/rails_ai_context/introspectors/recurring_schedules.rb +150 -0
- data/lib/rails_ai_context/introspectors/rendered_record.rb +87 -0
- data/lib/rails_ai_context/introspectors/retry_policy.rb +8 -5
- data/lib/rails_ai_context/introspectors/route_introspector.rb +153 -42
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +127 -40
- data/lib/rails_ai_context/introspectors/schema_dump_path.rb +80 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +346 -112
- data/lib/rails_ai_context/introspectors/schema_reader.rb +69 -29
- data/lib/rails_ai_context/introspectors/service_classes.rb +21 -15
- data/lib/rails_ai_context/introspectors/source_calls.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_introspector.rb +17 -2
- data/lib/rails_ai_context/introspectors/source_scan.rb +174 -26
- data/lib/rails_ai_context/introspectors/sqlite_virtual_tables.rb +28 -0
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +16 -5
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +372 -98
- data/lib/rails_ai_context/introspectors/superclass_chain.rb +14 -15
- data/lib/rails_ai_context/introspectors/table_name.rb +46 -45
- data/lib/rails_ai_context/introspectors/test_introspector.rb +289 -78
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +10 -5
- data/lib/rails_ai_context/introspectors/view_introspector.rb +5 -40
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +12 -10
- data/lib/rails_ai_context/method_name.rb +31 -0
- data/lib/rails_ai_context/migration_status.rb +5 -5
- data/lib/rails_ai_context/output_guard.rb +6 -4
- data/lib/rails_ai_context/package_json.rb +96 -5
- data/lib/rails_ai_context/path_resolver.rb +170 -15
- data/lib/rails_ai_context/payload.rb +48 -0
- data/lib/rails_ai_context/pending_migrations.rb +15 -18
- data/lib/rails_ai_context/portable_path.rb +76 -4
- data/lib/rails_ai_context/redaction.rb +8 -2
- data/lib/rails_ai_context/route_coverage.rb +5 -1
- data/lib/rails_ai_context/safe_file.rb +4 -1
- data/lib/rails_ai_context/safe_path.rb +61 -10
- data/lib/rails_ai_context/schema_hint.rb +8 -2
- data/lib/rails_ai_context/schema_version.rb +7 -9
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +13 -4
- data/lib/rails_ai_context/serializers/section_facts.rb +5 -1
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +2 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +7 -4
- data/lib/rails_ai_context/server.rb +26 -7
- data/lib/rails_ai_context/test_framework.rb +32 -5
- data/lib/rails_ai_context/tools/analyze_feature.rb +22 -10
- data/lib/rails_ai_context/tools/base_tool.rb +69 -30
- data/lib/rails_ai_context/tools/diagnose.rb +1 -1
- data/lib/rails_ai_context/tools/generate_test.rb +8 -9
- data/lib/rails_ai_context/tools/get_active_support.rb +19 -4
- data/lib/rails_ai_context/tools/get_api.rb +17 -11
- data/lib/rails_ai_context/tools/get_autoload.rb +10 -5
- data/lib/rails_ai_context/tools/get_callbacks.rb +65 -14
- data/lib/rails_ai_context/tools/get_component_catalog.rb +7 -4
- data/lib/rails_ai_context/tools/get_concern.rb +31 -18
- data/lib/rails_ai_context/tools/get_config.rb +52 -18
- data/lib/rails_ai_context/tools/get_controllers.rb +57 -13
- data/lib/rails_ai_context/tools/get_conventions.rb +44 -22
- data/lib/rails_ai_context/tools/get_engines.rb +2 -0
- data/lib/rails_ai_context/tools/get_env.rb +209 -52
- data/lib/rails_ai_context/tools/get_env_config.rb +38 -7
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +56 -7
- data/lib/rails_ai_context/tools/get_helper_methods.rb +136 -26
- data/lib/rails_ai_context/tools/get_job_pattern.rb +143 -87
- data/lib/rails_ai_context/tools/get_mailers.rb +59 -2
- data/lib/rails_ai_context/tools/get_model_details.rb +178 -28
- data/lib/rails_ai_context/tools/get_partial_interface.rb +101 -62
- data/lib/rails_ai_context/tools/get_routes.rb +35 -9
- data/lib/rails_ai_context/tools/get_schema.rb +160 -61
- data/lib/rails_ai_context/tools/get_service_pattern.rb +121 -25
- data/lib/rails_ai_context/tools/get_stimulus.rb +1 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +152 -82
- data/lib/rails_ai_context/tools/get_view.rb +37 -20
- data/lib/rails_ai_context/tools/migration_advisor.rb +64 -24
- data/lib/rails_ai_context/tools/onboard.rb +174 -88
- data/lib/rails_ai_context/tools/performance_check.rb +5 -13
- data/lib/rails_ai_context/tools/query.rb +122 -28
- data/lib/rails_ai_context/tools/read_logs.rb +45 -17
- data/lib/rails_ai_context/tools/runtime_info.rb +1 -1
- data/lib/rails_ai_context/tools/safe_call.rb +20 -15
- data/lib/rails_ai_context/tools/search_code.rb +114 -40
- data/lib/rails_ai_context/tools/search_docs.rb +22 -11
- data/lib/rails_ai_context/tools/security_scan.rb +51 -4
- data/lib/rails_ai_context/tools/validate.rb +21 -4
- data/lib/rails_ai_context/tools/validate_semantics.rb +18 -6
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +2 -2
- data/lib/rails_ai_context/view_file.rb +53 -3
- data/lib/rails_ai_context.rb +10 -2
- data/server.json +1 -1
- metadata +34 -3
|
@@ -13,147 +13,430 @@ module RailsAiContext
|
|
|
13
13
|
def initialize(include_initialize: false)
|
|
14
14
|
super()
|
|
15
15
|
@include_initialize = include_initialize
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@singleton_depth = 0
|
|
19
|
-
@inline_visibility_stack = [ {} ] # stack of { method_name => visibility }
|
|
16
|
+
@results = Results.new
|
|
17
|
+
@frames = [ Frame.new(:body, :public, {}) ]
|
|
20
18
|
@owner_stack = []
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
@
|
|
19
|
+
# Blocks that open a body of their own, keyed by their block node so
|
|
20
|
+
# the block's own enter/leave can open and close the frame.
|
|
21
|
+
@scoped_blocks = {}.compare_by_identity
|
|
24
22
|
@open_blocks = []
|
|
25
|
-
@
|
|
23
|
+
@def_depth = 0
|
|
24
|
+
@builders = []
|
|
25
|
+
# Each entry's parameters as written, which an alias of it takes on.
|
|
26
|
+
@param_slices = {}.compare_by_identity
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
#
|
|
29
|
+
# The methods found, plus the span of every def walked, listed or not: a call
|
|
30
|
+
# inside a skipped `initialize` still runs only when that method does.
|
|
31
|
+
class Results < Array
|
|
32
|
+
def def_spans
|
|
33
|
+
@def_spans ||= []
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# One visibility scope: a class or module body, a `class << self`
|
|
38
|
+
# body, or a block Rails evaluates as a module body. `marks` holds the
|
|
39
|
+
# inline `private :x` forms, keyed by [scope, name].
|
|
40
|
+
Frame = Struct.new(:kind, :visibility, :marks)
|
|
41
|
+
|
|
42
|
+
CLASS_SCOPE_FRAMES = %i[singleton class_methods].freeze
|
|
43
|
+
# A scope's or an association's block extends the relation, and
|
|
44
|
+
# `concern` builds a module the class does not include: a def in
|
|
45
|
+
# one is no method of the class.
|
|
46
|
+
BLOCK_FRAMES = {
|
|
47
|
+
class_methods: :class_methods, included: :included, concerning: :body,
|
|
48
|
+
scope: :extension, has_many: :extension, has_and_belongs_to_many: :extension,
|
|
49
|
+
has_one: :extension, belongs_to: :extension, concern: :extension
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
# Each defines a public method whatever `private` section it sits in;
|
|
53
|
+
# only Rails' `private: true` makes one private.
|
|
54
|
+
DELEGATORS = %i[delegate def_delegators def_instance_delegators def_delegator def_instance_delegator instance_delegate].freeze
|
|
55
|
+
|
|
56
|
+
# Ruby makes these instance methods private whatever section defines them.
|
|
57
|
+
ALWAYS_PRIVATE = %w[initialize initialize_copy initialize_clone initialize_dup respond_to_missing?].freeze
|
|
58
|
+
|
|
59
|
+
# Ruby's own definers: these honor the section's visibility, as `def` does.
|
|
60
|
+
ATTR_DEFINERS = { attr_reader: [ :reader ], attr_writer: [ :writer ], attr_accessor: %i[reader writer], attr: [ :reader ] }.freeze
|
|
61
|
+
# Active Support's: each defines public class and instance accessors from a string `class_eval`.
|
|
62
|
+
CLASS_ACCESSORS = {
|
|
63
|
+
class_attribute: %i[reader writer], mattr_reader: [ :reader ], cattr_reader: [ :reader ],
|
|
64
|
+
mattr_writer: [ :writer ], cattr_writer: [ :writer ], mattr_accessor: %i[reader writer], cattr_accessor: %i[reader writer]
|
|
65
|
+
}.freeze
|
|
66
|
+
# Sets, because a `when *names` allocates on every call node of every walk.
|
|
67
|
+
BLOCK_FRAME_NAMES = BLOCK_FRAMES.keys.to_set.freeze
|
|
68
|
+
DELEGATOR_NAMES = DELEGATORS.to_set.freeze
|
|
69
|
+
DEFINERS = Set[*ATTR_DEFINERS.keys, :define_method, :alias_method, *CLASS_ACCESSORS.keys].freeze
|
|
70
|
+
|
|
29
71
|
def on_class_node_enter(node)
|
|
30
|
-
|
|
31
|
-
@inline_visibility_stack.push({})
|
|
72
|
+
open_frame(:body)
|
|
32
73
|
@owner_stack.push(constant_path_string(node.constant_path))
|
|
33
74
|
end
|
|
34
75
|
|
|
35
76
|
def on_class_node_leave(node)
|
|
36
|
-
@
|
|
37
|
-
@inline_visibility_stack.pop
|
|
77
|
+
@frames.pop
|
|
38
78
|
@owner_stack.pop
|
|
39
79
|
end
|
|
40
80
|
|
|
41
81
|
def on_module_node_enter(node)
|
|
42
|
-
|
|
43
|
-
@inline_visibility_stack.push({})
|
|
82
|
+
open_frame(:body)
|
|
44
83
|
@owner_stack.push(constant_path_string(node.constant_path))
|
|
45
84
|
end
|
|
46
85
|
|
|
47
86
|
def on_module_node_leave(node)
|
|
48
|
-
@
|
|
49
|
-
@inline_visibility_stack.pop
|
|
87
|
+
@frames.pop
|
|
50
88
|
@owner_stack.pop
|
|
51
89
|
end
|
|
52
90
|
|
|
53
|
-
#
|
|
91
|
+
# A singleton class body starts public, and a `private` in it stays in it.
|
|
54
92
|
def on_singleton_class_node_enter(node)
|
|
55
|
-
|
|
56
|
-
@singleton_depth += 1
|
|
93
|
+
open_frame(:singleton)
|
|
57
94
|
end
|
|
58
95
|
|
|
59
96
|
def on_singleton_class_node_leave(node)
|
|
60
|
-
@
|
|
61
|
-
@in_singleton_class = false if @singleton_depth == 0
|
|
97
|
+
@frames.pop
|
|
62
98
|
end
|
|
63
99
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
100
|
+
# Bare `private` affects every later def in this frame; `private :x`
|
|
101
|
+
# and `private def x` mark the one name, retroactively for :x.
|
|
66
102
|
def on_call_node_enter(node)
|
|
103
|
+
@scoped_blocks[node.block] = :extension if new_class_body?(node) && !@scoped_blocks.key?(node.block)
|
|
67
104
|
return unless node.receiver.nil?
|
|
68
105
|
|
|
69
106
|
case node.name
|
|
70
107
|
when :private, :protected, :public
|
|
71
108
|
if node.arguments.nil?
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
@frames.last.visibility = node.name
|
|
110
|
+
else
|
|
111
|
+
mark(node.arguments.arguments, node.name, frame_scope)
|
|
112
|
+
end
|
|
113
|
+
when :private_class_method, :public_class_method
|
|
114
|
+
mark(Array(node.arguments&.arguments), node.name == :private_class_method ? :private : :public, :class)
|
|
115
|
+
when :module_function
|
|
116
|
+
if node.arguments.nil?
|
|
117
|
+
@frames.last.visibility = :module_function
|
|
74
118
|
else
|
|
75
|
-
|
|
76
|
-
# already-recorded methods and mark for future defs. The def in
|
|
77
|
-
# `private def x` is visited after this call, so marking its
|
|
78
|
-
# name here is enough.
|
|
79
|
-
args = node.arguments.arguments
|
|
80
|
-
args.each do |arg|
|
|
81
|
-
case arg
|
|
82
|
-
when Prism::DefNode
|
|
83
|
-
@inline_visibility_stack.last[arg.name.to_s] = node.name
|
|
84
|
-
when Prism::SymbolNode
|
|
85
|
-
method_name = arg.unescaped
|
|
86
|
-
@inline_visibility_stack.last[method_name] = node.name
|
|
87
|
-
existing = @results.find { |r| r[:name] == method_name && r[:owner] == @owner_stack }
|
|
88
|
-
existing[:visibility] = node.name if existing
|
|
89
|
-
end
|
|
90
|
-
end
|
|
119
|
+
mark(node.arguments.arguments, :module_function, :instance)
|
|
91
120
|
end
|
|
92
|
-
when
|
|
93
|
-
@
|
|
121
|
+
when BLOCK_FRAME_NAMES
|
|
122
|
+
@scoped_blocks[node.block] = BLOCK_FRAMES[node.name] if node.block.is_a?(Prism::BlockNode)
|
|
123
|
+
when DELEGATOR_NAMES
|
|
124
|
+
record_delegated(node) if @def_depth.zero?
|
|
125
|
+
when DEFINERS
|
|
126
|
+
record_defined(node) if @def_depth.zero? && @frames.last.kind != :extension
|
|
94
127
|
end
|
|
95
128
|
end
|
|
96
129
|
|
|
97
|
-
#
|
|
98
|
-
|
|
130
|
+
# `Name = Struct.new do ... end` is Name's body.
|
|
131
|
+
def on_constant_write_node_enter(node)
|
|
132
|
+
open_builder(node, node.name.to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def on_constant_write_node_leave(node)
|
|
136
|
+
close_builder(node)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def on_constant_path_write_node_enter(node)
|
|
140
|
+
open_builder(node, constant_path_string(node.target))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def on_constant_path_write_node_leave(node)
|
|
144
|
+
close_builder(node)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ponytail: a Module.new held in a local is read as the enclosing class's, which is where it is
|
|
148
|
+
# nearly always included; follow the local to its `include` if another use turns up.
|
|
149
|
+
def on_local_variable_write_node_enter(node)
|
|
150
|
+
value = node.value
|
|
151
|
+
@scoped_blocks[value.block] = :body if value.is_a?(Prism::CallNode) && new_class_body?(value) && value.name == :new &&
|
|
152
|
+
value.receiver.name == :Module
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def on_alias_method_node_enter(node)
|
|
156
|
+
return unless @def_depth.zero? && @frames.last.kind != :extension
|
|
157
|
+
|
|
158
|
+
new_name = literal_string(node.new_name)
|
|
159
|
+
old_name = literal_string(node.old_name)
|
|
160
|
+
record_alias(node, new_name, old_name) if new_name && old_name
|
|
161
|
+
end
|
|
162
|
+
|
|
99
163
|
def on_block_node_enter(node)
|
|
100
|
-
kind = @
|
|
164
|
+
kind = @scoped_blocks.delete(node)
|
|
101
165
|
return unless kind
|
|
102
166
|
|
|
103
167
|
@open_blocks.push(node)
|
|
104
|
-
|
|
105
|
-
@visibility_stack.push(:public)
|
|
106
|
-
@inline_visibility_stack.push({})
|
|
168
|
+
open_frame(kind)
|
|
107
169
|
end
|
|
108
170
|
|
|
109
171
|
def on_block_node_leave(node)
|
|
110
172
|
return unless @open_blocks.last.equal?(node)
|
|
111
173
|
|
|
112
174
|
@open_blocks.pop
|
|
113
|
-
@
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
@frames.pop
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def on_def_node_leave(node)
|
|
179
|
+
@def_depth -= 1
|
|
116
180
|
end
|
|
117
181
|
|
|
118
182
|
def on_def_node_enter(node)
|
|
119
|
-
|
|
183
|
+
@def_depth += 1
|
|
184
|
+
@results.def_spans << (node.location.start_offset...node.location.end_offset)
|
|
185
|
+
scope = def_scope(node)
|
|
120
186
|
method_name = node.name.to_s
|
|
187
|
+
return unless scope
|
|
188
|
+
return if @frames.last.kind == :extension || class_methods_module_own?(node.receiver)
|
|
189
|
+
return if method_name == "initialize" && scope == :instance && !@include_initialize
|
|
190
|
+
|
|
191
|
+
# A bare `private` reaches only defs written without a receiver.
|
|
192
|
+
visibility = ruby_visibility(scope, method_name, node.receiver ? :public : @frames.last.visibility)
|
|
193
|
+
if visibility == :module_function
|
|
194
|
+
record(node, method_name, :instance, :private, prefixed: false)
|
|
195
|
+
@results.last[:module_function] = true
|
|
196
|
+
record(node, method_name, :class, :public, prefixed: true)
|
|
197
|
+
else
|
|
198
|
+
record(node, method_name, scope, visibility, prefixed: !node.receiver.nil?)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
# These evaluate their block as a new class or module body.
|
|
205
|
+
CLASS_BUILDERS = { Struct: :new, Class: :new, Module: :new, Data: :define }.freeze
|
|
206
|
+
|
|
207
|
+
def new_class_body?(node)
|
|
208
|
+
receiver = node.receiver
|
|
209
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
210
|
+
return false unless receiver.is_a?(Prism::ConstantReadNode) || (receiver.is_a?(Prism::ConstantPathNode) && receiver.parent.nil?)
|
|
211
|
+
|
|
212
|
+
CLASS_BUILDERS[receiver.name] == node.name
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def open_builder(node, name)
|
|
216
|
+
value = node.value
|
|
217
|
+
return unless value.is_a?(Prism::CallNode) && new_class_body?(value)
|
|
218
|
+
|
|
219
|
+
@scoped_blocks[value.block] = :body
|
|
220
|
+
@owner_stack.push(name)
|
|
221
|
+
@builders.push(node)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def close_builder(node)
|
|
225
|
+
return unless @builders.last.equal?(node)
|
|
226
|
+
|
|
227
|
+
@builders.pop
|
|
228
|
+
@owner_stack.pop
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# What an includer gains: a `class_methods` def, or a class method written in `included do`.
|
|
232
|
+
def includer_gains?(scope)
|
|
233
|
+
frame = @frames.last
|
|
234
|
+
return true if frame.kind == :class_methods
|
|
235
|
+
|
|
236
|
+
frame = @frames[-2] if frame.kind == :singleton
|
|
237
|
+
frame&.kind == :included && scope == :class
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# `def self.x` in `class_methods do`, or anything in its `class << self`, is ClassMethods' own:
|
|
241
|
+
# neither the includer nor the concern responds to it.
|
|
242
|
+
def class_methods_module_own?(receiver)
|
|
243
|
+
kinds = @frames.last(2).map(&:kind)
|
|
244
|
+
(kinds.last == :class_methods && !receiver.nil?) || kinds == %i[class_methods singleton]
|
|
245
|
+
end
|
|
121
246
|
|
|
122
|
-
|
|
247
|
+
def open_frame(kind)
|
|
248
|
+
@frames.push(Frame.new(kind, :public, {}))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def ruby_visibility(scope, name, visibility)
|
|
252
|
+
@frames.last.marks.fetch([ scope, name ]) { scope == :instance && ALWAYS_PRIVATE.include?(name) ? :private : visibility }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def frame_scope
|
|
256
|
+
CLASS_SCOPE_FRAMES.include?(@frames.last.kind) ? :class : :instance
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# nil for a singleton def on anything but this class (`def other.x`):
|
|
260
|
+
# it is no method of the class.
|
|
261
|
+
def def_scope(node)
|
|
262
|
+
receiver = node.receiver
|
|
263
|
+
return frame_scope if receiver.nil?
|
|
264
|
+
return :class if receiver.is_a?(Prism::SelfNode)
|
|
265
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
266
|
+
|
|
267
|
+
owner = @owner_stack.join("::")
|
|
268
|
+
name = constant_path_string(receiver).delete_prefix("::")
|
|
269
|
+
:class if owner == name || owner.end_with?("::#{name}")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def mark(args, visibility, scope)
|
|
273
|
+
marks = @frames.last.marks
|
|
274
|
+
args.each do |arg|
|
|
275
|
+
case arg
|
|
276
|
+
when Prism::DefNode
|
|
277
|
+
marks[[ def_scope(arg), arg.name.to_s ]] = visibility
|
|
278
|
+
when Prism::SymbolNode
|
|
279
|
+
name = arg.unescaped
|
|
280
|
+
marks[[ scope, name ]] = visibility
|
|
281
|
+
existing = @results.reverse_each.find { |r| r[:name] == name && r[:scope] == scope && r[:owner] == @owner_stack }
|
|
282
|
+
apply_visibility(existing, visibility) if existing
|
|
283
|
+
when Prism::CallNode
|
|
284
|
+
# Since Ruby 3.0 these return the names they define, so `private attr_reader :x` works.
|
|
285
|
+
defined_names(arg).each { |name| marks[[ scope, name ]] = visibility } unless visibility == :module_function
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
123
289
|
|
|
124
|
-
|
|
125
|
-
|
|
290
|
+
def defined_names(call)
|
|
291
|
+
return [] unless call.receiver.nil?
|
|
126
292
|
|
|
127
|
-
|
|
293
|
+
names = Array(call.arguments&.arguments).filter_map { |a| literal_string(a) }
|
|
294
|
+
case call.name
|
|
295
|
+
when :define_method, :alias_method then names.first(1)
|
|
296
|
+
when *ATTR_DEFINERS.keys
|
|
297
|
+
kinds = ATTR_DEFINERS[call.name]
|
|
298
|
+
names.flat_map { |name| [ (name if kinds.include?(:reader)), ("#{name}=" if kinds.include?(:writer)) ].compact }
|
|
299
|
+
else []
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def apply_visibility(entry, visibility)
|
|
304
|
+
return entry[:visibility] = visibility unless visibility == :module_function
|
|
128
305
|
|
|
129
|
-
|
|
306
|
+
entry[:visibility] = :private
|
|
307
|
+
entry[:module_function] = true
|
|
308
|
+
@results << entry.merge(scope: :class, visibility: :public, signature: "self.#{entry[:signature]}").except(:module_function)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def record(node, method_name, scope, visibility, prefixed:)
|
|
312
|
+
slices = parameter_slices(node.parameters)
|
|
313
|
+
entry = {
|
|
130
314
|
name: method_name,
|
|
131
|
-
scope:
|
|
315
|
+
scope: scope,
|
|
132
316
|
visibility: visibility,
|
|
133
|
-
params:
|
|
317
|
+
params: extract_params(node),
|
|
134
318
|
# Enclosing class/module names, outermost first. A caller that
|
|
135
319
|
# wants one class's own methods needs this: a helper class nested
|
|
136
320
|
# inside a service is a separate owner, not part of its interface.
|
|
137
321
|
owner: @owner_stack.dup,
|
|
138
322
|
# Sliced off the node so defaults read as written (`options = {}`);
|
|
139
323
|
# `params` records names only.
|
|
140
|
-
signature:
|
|
324
|
+
signature: signature_text("#{'self.' if prefixed}#{node.name}", slices),
|
|
141
325
|
location: node.location.start_line,
|
|
142
326
|
end_location: node.location.end_line,
|
|
327
|
+
# Offsets, unlike lines, tell a call that shares a line with a def from one inside it.
|
|
328
|
+
offset: node.location.start_offset,
|
|
329
|
+
end_offset: node.location.end_offset,
|
|
143
330
|
confidence: RailsAiContext::Confidence::VERIFIED
|
|
144
331
|
}
|
|
332
|
+
# Outside `included do`, a `def self.x` or `class << self` method stays on the module.
|
|
333
|
+
entry[:class_methods_block] = true if includer_gains?(scope)
|
|
334
|
+
@param_slices[entry] = slices
|
|
335
|
+
@results << entry
|
|
145
336
|
end
|
|
146
337
|
|
|
147
|
-
|
|
338
|
+
def record_defined(node)
|
|
339
|
+
names = Array(node.arguments&.arguments).filter_map { |a| literal_string(a) }
|
|
340
|
+
case node.name
|
|
341
|
+
when :define_method
|
|
342
|
+
record_name(node, names.first, frame_scope, @frames.last.visibility) if names.first
|
|
343
|
+
when :alias_method
|
|
344
|
+
record_alias(node, *names.first(2)) if names.size >= 2
|
|
345
|
+
when *ATTR_DEFINERS.keys
|
|
346
|
+
names.each { |name| record_accessors(node, name, ATTR_DEFINERS[node.name], frame_scope, @frames.last.visibility) }
|
|
347
|
+
else
|
|
348
|
+
record_class_accessors(node, names)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def record_class_accessors(node, names)
|
|
353
|
+
kinds = CLASS_ACCESSORS[node.name]
|
|
354
|
+
options = extract_keyword_options(node)
|
|
355
|
+
off = ->(key) { options[key] == false || options[:instance_accessor] == false }
|
|
356
|
+
instance = kinds.reject { |kind| off.call(kind == :reader ? :instance_reader : :instance_writer) }
|
|
357
|
+
predicate = node.name == :class_attribute && options[:instance_predicate] != false
|
|
358
|
+
names.each do |name|
|
|
359
|
+
record_accessors(node, name, kinds, :class, :public)
|
|
360
|
+
record_name(node, "#{name}?", :class, :public) if predicate
|
|
361
|
+
record_accessors(node, name, instance, :instance, :public)
|
|
362
|
+
record_name(node, "#{name}?", :instance, :public) if predicate && instance.include?(:reader)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def record_accessors(node, name, kinds, scope, visibility)
|
|
367
|
+
record_name(node, name, scope, visibility) if kinds.include?(:reader)
|
|
368
|
+
record_name(node, "#{name}=", scope, visibility, signature: "#{name}=(value)") if kinds.include?(:writer)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# An alias carries its original's visibility and parameters, whatever section it sits in.
|
|
372
|
+
def record_alias(node, new_name, old_name)
|
|
373
|
+
scope = frame_scope
|
|
374
|
+
original = @results.reverse_each.find { |r| r[:name] == old_name && r[:scope] == scope && r[:owner] == @owner_stack }
|
|
375
|
+
slices = original ? @param_slices.fetch(original, []) : []
|
|
376
|
+
record_name(node, new_name, scope, original ? original[:visibility] : :public,
|
|
377
|
+
signature: signature_text(new_name, slices), params: original ? original[:params].dup : [], slices: slices)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def record_name(node, name, scope, visibility, signature: name, params: [], slices: [])
|
|
381
|
+
return if class_methods_module_own?(nil)
|
|
382
|
+
|
|
383
|
+
visibility = :public if visibility == :module_function
|
|
384
|
+
entry = {
|
|
385
|
+
name: name,
|
|
386
|
+
scope: scope,
|
|
387
|
+
visibility: ruby_visibility(scope, name, visibility),
|
|
388
|
+
params: params,
|
|
389
|
+
owner: @owner_stack.dup,
|
|
390
|
+
signature: signature,
|
|
391
|
+
# No end: these have no body for a call to sit inside.
|
|
392
|
+
location: node.location.start_line,
|
|
393
|
+
confidence: node.is_a?(Prism::CallNode) ? confidence_for(node) : RailsAiContext::Confidence::VERIFIED
|
|
394
|
+
}
|
|
395
|
+
entry[:class_methods_block] = true if includer_gains?(scope)
|
|
396
|
+
@param_slices[entry] = slices
|
|
397
|
+
@results << entry
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def record_delegated(node)
|
|
401
|
+
return if @frames.last.kind == :extension
|
|
402
|
+
|
|
403
|
+
args = node.arguments&.arguments || []
|
|
404
|
+
positional = args.reject { |a| a.is_a?(Prism::KeywordHashNode) }
|
|
405
|
+
options = extract_keyword_options(node)
|
|
406
|
+
visibility = :public
|
|
407
|
+
names = case node.name
|
|
408
|
+
when :def_delegators, :def_instance_delegators
|
|
409
|
+
positional.drop(1).filter_map { |a| literal_string(a) } - %w[__send__ __id__]
|
|
410
|
+
when :def_delegator, :def_instance_delegator
|
|
411
|
+
Array(literal_string(positional[2] || positional[1]))
|
|
412
|
+
else
|
|
413
|
+
if options.key?(:to)
|
|
414
|
+
visibility = :private if options[:private] == true
|
|
415
|
+
prefix = delegation_prefix(options)
|
|
416
|
+
return unless prefix
|
|
417
|
+
|
|
418
|
+
positional.filter_map { |a| literal_string(a) }.map { |name| "#{prefix}#{name}" }
|
|
419
|
+
else
|
|
420
|
+
# Forwardable's `delegate [:a, :b] => :@x`.
|
|
421
|
+
args.grep(Prism::KeywordHashNode).flat_map(&:elements).grep(Prism::AssocNode).flat_map { |assoc| literal_strings(assoc.key) }
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
names.each { |name| record_name(node, name, frame_scope, visibility) }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def delegation_prefix(options)
|
|
428
|
+
case options[:prefix]
|
|
429
|
+
# Rails raises on `prefix: true` with an ivar target, so nothing is defined.
|
|
430
|
+
when true then options[:to].to_s.start_with?("@") ? nil : "#{options[:to]}_"
|
|
431
|
+
when Symbol, String then "#{options[:prefix]}_"
|
|
432
|
+
else ""
|
|
433
|
+
end
|
|
434
|
+
end
|
|
148
435
|
|
|
149
436
|
# `class << self` members carry no receiver of their own, so they read
|
|
150
437
|
# as the bare name, which is how they are written.
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
params = parameter_slices(node.parameters)
|
|
154
|
-
return "#{prefix}#{node.name}" if params.empty?
|
|
155
|
-
|
|
156
|
-
"#{prefix}#{node.name}(#{params.join(', ')})"
|
|
438
|
+
def signature_text(name, slices)
|
|
439
|
+
slices.empty? ? name : "#{name}(#{slices.join(', ')})"
|
|
157
440
|
end
|
|
158
441
|
|
|
159
442
|
# Each parameter is sliced on its own rather than taking the whole list
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
module Introspectors
|
|
5
5
|
module Listeners
|
|
6
|
-
# What a replay needs beyond the DSL listeners: down-only
|
|
7
|
-
#
|
|
6
|
+
# What a replay needs beyond the DSL listeners: down-only and revert ranges,
|
|
7
|
+
# a block's `t.` statements, and the top-level statements the DSL listeners leave out.
|
|
8
8
|
class MigrationReplayListener < BaseListener
|
|
9
9
|
include SchemaDslListener::TableBlock
|
|
10
10
|
|
|
@@ -19,16 +19,17 @@ module RailsAiContext
|
|
|
19
19
|
remove: :remove_columns, remove_references: :remove_reference, remove_belongs_to: :remove_reference,
|
|
20
20
|
remove_index: :remove_index, rename: :rename_column, remove_timestamps: :remove_columns,
|
|
21
21
|
change: :change_column, change_default: :change_column_default, change_null: :change_column_null,
|
|
22
|
-
timestamps: :add_timestamps
|
|
22
|
+
timestamps: :add_timestamps, check_constraint: :add_check_constraint, remove_check_constraint: :remove_check_constraint
|
|
23
23
|
}.freeze
|
|
24
24
|
|
|
25
25
|
def on_call_node_enter(node)
|
|
26
26
|
note_block(node)
|
|
27
27
|
args = node.arguments&.arguments || []
|
|
28
28
|
if %i[down revert].include?(node.name) && node.block
|
|
29
|
-
@results << { kind:
|
|
30
|
-
elsif node.name == :
|
|
31
|
-
|
|
29
|
+
@results << { kind: node.name, range: node.location.start_line..node.location.end_line }
|
|
30
|
+
elsif node.name == :revert && node.receiver.nil? && args.any?
|
|
31
|
+
# `revert SomeMigration` runs another file's statements inverted, which this file does not hold.
|
|
32
|
+
@results << { kind: :not_replayed, location: node.location.start_line }
|
|
32
33
|
elsif TABLE_OPS[node.name] == :remove_reference && block_column?(node.receiver)
|
|
33
34
|
# Table#remove_references drops each name it is given.
|
|
34
35
|
args.reject { |arg| arg.is_a?(Prism::KeywordHashNode) }.each do |arg|
|
|
@@ -40,29 +41,43 @@ module RailsAiContext
|
|
|
40
41
|
tables = args.reject { |arg| arg.is_a?(Prism::KeywordHashNode) }.first(2).map { |arg| literal_string(arg) }
|
|
41
42
|
@results << { action: :create_join_table, tables: tables, options: extract_keyword_options(node),
|
|
42
43
|
location: node.location.start_line }
|
|
43
|
-
elsif %i[remove_columns add_timestamps].include?(node.name) && node.receiver.nil?
|
|
44
|
+
elsif %i[remove_columns add_timestamps rename_index add_check_constraint remove_check_constraint].include?(node.name) && node.receiver.nil?
|
|
44
45
|
@results << top_level(node, args)
|
|
45
46
|
elsif node.name == :execute && node.receiver.nil?
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
sql_node = args.first
|
|
48
|
+
sql_node = sql_node.receiver if sql_node.is_a?(Prism::CallNode) && sql_node.name == :squish && sql_node.arguments.nil?
|
|
49
|
+
statements = sql_statements(sql_node)
|
|
50
|
+
# SQL built at run time that creates or drops a table is counted as not replayed.
|
|
51
|
+
if statements.nil?
|
|
52
|
+
literal = sql_node.respond_to?(:parts) ? sql_node.parts.grep(Prism::StringNode).map(&:unescaped).join : ""
|
|
53
|
+
@results << { kind: :not_replayed, location: node.location.start_line } if literal.match?(/\b(?:CREATE|DROP)\s+TABLE\b/i)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
statements.each do |sql|
|
|
57
|
+
if (match = DROP_TABLE.match(sql))
|
|
58
|
+
match[1].split(",").each do |name|
|
|
59
|
+
@results << { action: :drop_table, table: name.strip.delete('"`').split(".").last, options: {}, location: node.location.start_line }
|
|
60
|
+
end
|
|
61
|
+
elsif sql.match?(CREATE_TABLE)
|
|
62
|
+
@results << { action: :create_table_sql, sql: sql, location: node.location.start_line }
|
|
63
|
+
end
|
|
48
64
|
end
|
|
49
65
|
end
|
|
50
66
|
end
|
|
51
67
|
|
|
52
68
|
DROP_TABLE = /\A\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?((?:[\w."`]+\s*,\s*)*[\w."`]+)(?:\s+(?:CASCADE|RESTRICT))?\s*\z/i
|
|
69
|
+
CREATE_TABLE = /\A\s*CREATE\s+TABLE\s/i
|
|
53
70
|
|
|
54
71
|
private
|
|
55
72
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
node.unescaped.split(";").flat_map do |sql|
|
|
63
|
-
match = DROP_TABLE.match(sql) or next []
|
|
64
|
-
match[1].split(",").map { |name| name.strip.delete('"`').split(".").last }
|
|
73
|
+
# A literal SQL string's statements, which drop or create tables here; nil for a string built at run time.
|
|
74
|
+
def sql_statements(node)
|
|
75
|
+
sql = case node
|
|
76
|
+
when Prism::StringNode then node.unescaped
|
|
77
|
+
# A squiggly heredoc over several lines parses as one string per line.
|
|
78
|
+
when Prism::InterpolatedStringNode then adjacent_literals(node)
|
|
65
79
|
end
|
|
80
|
+
sql.split(";") unless sql.nil? || sql == RailsAiContext::Confidence::INFERRED
|
|
66
81
|
end
|
|
67
82
|
|
|
68
83
|
# A statement naming a table it cannot read is counted, never given another table.
|
|
@@ -83,24 +98,18 @@ module RailsAiContext
|
|
|
83
98
|
# remove_index takes one column or an array of them.
|
|
84
99
|
when :remove_index then result[:columns] = literal_strings(positional.first)
|
|
85
100
|
when :rename_column then result.merge!(column: names[0], new_name: names[1])
|
|
101
|
+
when :rename_index then result.merge!(old_name: names[0], new_name: names[1])
|
|
86
102
|
when :change_column then result.merge!(column: names[0], column_type: names[1])
|
|
87
103
|
when :change_column_null then result.merge!(column: names[0], null: boolean_value(positional[1]))
|
|
88
104
|
when :change_column_default
|
|
89
105
|
result[:column] = names[0]
|
|
90
106
|
result[:new_default] = extract_value(positional[1]) if positional[1]
|
|
91
107
|
when :add_timestamps then result.merge!(default_source: default_source(node), default_proc: proc_default?(node))
|
|
108
|
+
when :add_check_constraint, :remove_check_constraint then result[:expression] = names[0]
|
|
92
109
|
end
|
|
93
110
|
result
|
|
94
111
|
end
|
|
95
112
|
|
|
96
|
-
# t.column is TableDefinition#column: a column definition like t.string.
|
|
97
|
-
def column_definition(node, args)
|
|
98
|
-
positional = args.reject { |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
99
|
-
{ type: :column, name: literal_string(positional[0]), column_type: literal_string(positional[1]),
|
|
100
|
-
options: extract_keyword_options(node), default_source: default_source(node), default_proc: proc_default?(node),
|
|
101
|
-
location: node.location.start_line }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
113
|
# The block parameter a create_table body names `t`, whether it reads
|
|
105
114
|
# as a local variable or as a bare call.
|
|
106
115
|
def block_column?(receiver)
|