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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# Calls on an HTTP client constant with a literal URL: `Faraday.get("https://x")`,
|
|
7
|
+
# `Net::HTTP.get(URI("https://x"))`, `Faraday.new(url: "https://x")`, `URI.open("https://x")`,
|
|
8
|
+
# and `Net::HTTP.start("api.x.com", 443)`, whose first argument is a host.
|
|
9
|
+
class HttpClientCallListener < BaseListener
|
|
10
|
+
CLIENTS = %w[Faraday Net::HTTP HTTParty RestClient HTTP Excon Typhoeus].freeze
|
|
11
|
+
|
|
12
|
+
def on_call_node_enter(node)
|
|
13
|
+
client = client_name(node) or return
|
|
14
|
+
|
|
15
|
+
first = node.arguments&.arguments&.first
|
|
16
|
+
if client == "Net::HTTP" && %i[start new].include?(node.name)
|
|
17
|
+
host = literal_string(first)
|
|
18
|
+
@results << { client: client, host: host, line: node.location.start_line } if host
|
|
19
|
+
elsif (url = url_literal(first) || url_literal(extract_keyword_nodes(node)[:url]))
|
|
20
|
+
@results << { client: client, url: url, line: node.location.start_line }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def client_name(node)
|
|
27
|
+
receiver = node.receiver
|
|
28
|
+
return nil unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
29
|
+
|
|
30
|
+
name = constant_path_string(receiver)
|
|
31
|
+
return "URI.open" if name == "URI" && node.name == :open
|
|
32
|
+
|
|
33
|
+
name if CLIENTS.include?(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A string literal, or one wrapped in `URI(...)` or `URI.parse(...)`.
|
|
37
|
+
def url_literal(node)
|
|
38
|
+
return node.unescaped if node.is_a?(Prism::StringNode)
|
|
39
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
40
|
+
|
|
41
|
+
receiver = node.receiver
|
|
42
|
+
wrapped = (receiver.nil? && node.name == :URI) ||
|
|
43
|
+
(node.name == :parse && receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :URI)
|
|
44
|
+
inner = node.arguments&.arguments&.first
|
|
45
|
+
inner.unescaped if wrapped && inner.is_a?(Prism::StringNode)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# Locale files an app adds to I18n's load path, literal paths and globs only:
|
|
7
|
+
#
|
|
8
|
+
# config.i18n.load_path += Dir[Rails.root.join("my/locales/*.{rb,yml}")] → "my/locales/*.{rb,yml}"
|
|
9
|
+
# I18n.load_path << "#{Rails.root}/lib/locales/de.yml" → "lib/locales/de.yml"
|
|
10
|
+
# config.i18n.load_path = Dir[Rails.root.join("x/*.yml")] → "x/*.yml"
|
|
11
|
+
#
|
|
12
|
+
# The path forms are LiteralPaths', plus the Dir[] / Dir.glob wrapper.
|
|
13
|
+
class I18nLoadPathListener < BaseListener
|
|
14
|
+
include LiteralPaths
|
|
15
|
+
|
|
16
|
+
def on_call_node_enter(node)
|
|
17
|
+
if node.name == :load_path= && i18n?(node.receiver)
|
|
18
|
+
collect_paths(node.arguments&.arguments&.first)
|
|
19
|
+
elsif APPENDING.include?(node.name) && load_path?(node.receiver)
|
|
20
|
+
Array(node.arguments&.arguments).each { |argument| collect_paths(argument) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def on_call_operator_write_node_enter(node)
|
|
25
|
+
return unless node.binary_operator == :+ && node.read_name == :load_path && i18n?(node.receiver)
|
|
26
|
+
|
|
27
|
+
collect_paths(node.value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def load_path?(node)
|
|
33
|
+
node.is_a?(Prism::CallNode) && node.name == :load_path && i18n?(node.receiver)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def i18n?(node)
|
|
37
|
+
case node
|
|
38
|
+
when Prism::CallNode then node.name == :i18n
|
|
39
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then node.slice.delete_prefix("::") == "I18n"
|
|
40
|
+
else false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def collect_call_path(node)
|
|
45
|
+
if %i[[] glob].include?(node.name) && node.receiver&.slice&.delete_prefix("::") == "Dir"
|
|
46
|
+
return collect_paths(node.arguments&.arguments&.first)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module RailsAiContext
|
|
6
|
+
module Introspectors
|
|
7
|
+
module Listeners
|
|
8
|
+
# The literal path forms an app writes a setting with: a string, `%W(#{config.root}/x)`,
|
|
9
|
+
# `Rails.root.join("x")`. Each one read lands in `push_path`; a path built at run time stays unread.
|
|
10
|
+
# `collect_paths` returns whether it read the whole node, so a caller can tell a partly read write.
|
|
11
|
+
module LiteralPaths
|
|
12
|
+
APPENDING = %i[<< push append concat].to_set.freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def paths_receiver?(node)
|
|
17
|
+
node.is_a?(Prism::CallNode) && node.name == :paths
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def collect_paths(node)
|
|
21
|
+
case node
|
|
22
|
+
when Prism::ArrayNode then node.elements.map { |element| collect_paths(element) }.all?
|
|
23
|
+
when Prism::StringNode then push_path(node.unescaped)
|
|
24
|
+
when Prism::InterpolatedStringNode then collect_interpolated_path(node)
|
|
25
|
+
when Prism::CallNode then collect_call_path(node)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `"#{config.root}/lib_static"`, and only that: a gem's root names a path outside
|
|
30
|
+
# the app, and a later interpolation leaves the path unknown.
|
|
31
|
+
def collect_interpolated_path(node)
|
|
32
|
+
return unless app_root?(node.parts.first) && node.parts.drop(1).all?(Prism::StringNode)
|
|
33
|
+
|
|
34
|
+
push_path(node.parts.drop(1).map(&:content).join)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `Rails.root.join("lib_static")` and its `.to_s`, whose arguments are
|
|
38
|
+
# the path relative to the app root.
|
|
39
|
+
def collect_call_path(node)
|
|
40
|
+
return collect_file_path(node) if @file && file_constant?(node.receiver)
|
|
41
|
+
return collect_paths(node.receiver) unless node.name == :join
|
|
42
|
+
|
|
43
|
+
segments = app_root_join(node)
|
|
44
|
+
push_path(File.join(*segments)) if segments
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# `File.expand_path("x", __dir__)` and `File.join(__dir__, "x")`, read when the
|
|
48
|
+
# listener is given `@file`, the walked file's path relative to the app root.
|
|
49
|
+
def collect_file_path(node)
|
|
50
|
+
arguments = Array(node.arguments&.arguments)
|
|
51
|
+
path = case node.name
|
|
52
|
+
when :expand_path then file_relative(arguments.first, arguments[1]) if arguments.size == 2
|
|
53
|
+
when :join then file_relative_join(arguments)
|
|
54
|
+
end
|
|
55
|
+
push_path(path) if path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def file_relative(relative, base)
|
|
59
|
+
return unless relative.is_a?(Prism::StringNode)
|
|
60
|
+
|
|
61
|
+
anchor = file_anchor(base) or return
|
|
62
|
+
Pathname.new(File.join(anchor, relative.unescaped)).cleanpath.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def file_relative_join(arguments)
|
|
66
|
+
anchor = file_anchor(arguments.first) or return
|
|
67
|
+
rest = arguments.drop(1)
|
|
68
|
+
return unless rest.any? && rest.all?(Prism::StringNode)
|
|
69
|
+
|
|
70
|
+
Pathname.new(File.join(anchor, *rest.map(&:unescaped))).cleanpath.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# `File.expand_path("x", __FILE__)` resolves against the file name itself, as Ruby does.
|
|
74
|
+
def file_anchor(node)
|
|
75
|
+
case node
|
|
76
|
+
when Prism::SourceFileNode then @file
|
|
77
|
+
when Prism::CallNode then File.dirname(@file) if dir_call?(node) || file_dirname_call?(node)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `File.dirname(__FILE__)`, the older spelling of `__dir__`.
|
|
82
|
+
def file_dirname_call?(node)
|
|
83
|
+
arguments = Array(node.arguments&.arguments)
|
|
84
|
+
node.name == :dirname && file_constant?(node.receiver) && arguments.size == 1 && arguments.first.is_a?(Prism::SourceFileNode)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The segments of `Rails.root.join("a", "b")`, nil unless every one is a literal.
|
|
88
|
+
def app_root_join(node)
|
|
89
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :join && app_root?(node.receiver)
|
|
90
|
+
|
|
91
|
+
segments = Array(node.arguments&.arguments)
|
|
92
|
+
segments.map(&:unescaped) if segments.any? && segments.all?(Prism::StringNode)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# `root`, `config.root`, `Rails.root` or `::Rails.root`.
|
|
96
|
+
def app_root?(node)
|
|
97
|
+
node = node.statements if node.is_a?(Prism::EmbeddedStatementsNode)
|
|
98
|
+
node = node.body.first if node.is_a?(Prism::StatementsNode)
|
|
99
|
+
return false unless node.is_a?(Prism::CallNode) && node.name == :root && node.arguments.nil? && node.block.nil?
|
|
100
|
+
|
|
101
|
+
receiver = node.receiver
|
|
102
|
+
case receiver
|
|
103
|
+
when nil then true
|
|
104
|
+
when Prism::CallNode then receiver.name == :config && receiver.receiver.nil? && receiver.arguments.nil?
|
|
105
|
+
when Prism::LocalVariableReadNode then receiver.name == :config
|
|
106
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then constant_path_string(receiver) == "Rails"
|
|
107
|
+
else false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def push_path(value)
|
|
112
|
+
cleaned = value.to_s.strip.delete_prefix("/")
|
|
113
|
+
record_path(cleaned) unless cleaned.empty?
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def record_path(path)
|
|
118
|
+
@results << path
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -9,6 +9,8 @@ module RailsAiContext
|
|
|
9
9
|
# broadcasts, generates_token_for, attribute, etc.
|
|
10
10
|
class MacrosListener < BaseListener
|
|
11
11
|
include WithOptionsScope
|
|
12
|
+
include OwnerScope
|
|
13
|
+
include BranchConditions
|
|
12
14
|
|
|
13
15
|
SIMPLE_MACROS = %i[
|
|
14
16
|
has_secure_password
|
|
@@ -16,24 +18,109 @@ module RailsAiContext
|
|
|
16
18
|
|
|
17
19
|
ATTRIBUTE_MACROS = %i[
|
|
18
20
|
encrypts normalizes has_one_attached has_many_attached
|
|
19
|
-
has_rich_text generates_token_for serialize
|
|
21
|
+
has_rich_text generates_token_for serialize attr_readonly query_constraints
|
|
20
22
|
].to_set.freeze
|
|
21
23
|
|
|
24
|
+
# Class settings each change how the model reads, writes or loads; kept as written.
|
|
25
|
+
SETTINGS = %i[
|
|
26
|
+
inheritance_column= store_full_sti_class= strict_loading_by_default=
|
|
27
|
+
implicit_order_column= locking_column=
|
|
28
|
+
].to_set.freeze
|
|
29
|
+
|
|
30
|
+
STORE_MACROS = %i[store store_accessor].to_set.freeze
|
|
31
|
+
|
|
22
32
|
BROADCAST_MACROS = %i[
|
|
23
33
|
broadcasts broadcasts_to broadcasts_refreshes_to
|
|
24
34
|
].to_set.freeze
|
|
25
35
|
|
|
36
|
+
# Model gems' class macros, each listed as written; aasm's block is read in full.
|
|
37
|
+
GEM_MACROS = %i[
|
|
38
|
+
has_paper_trail audited acts_as_paranoid friendly_id
|
|
39
|
+
mount_uploader mount_uploaders monetize
|
|
40
|
+
pg_search_scope multisearchable searchkick
|
|
41
|
+
acts_as_list acts_as_tenant multi_tenant acts_as_taggable acts_as_taggable_on
|
|
42
|
+
has_ancestry acts_as_nested_set has_closure_tree acts_as_tree
|
|
43
|
+
state_machine workflow
|
|
44
|
+
].to_set.freeze
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
super
|
|
48
|
+
@def_depth = 0
|
|
49
|
+
@eval_depths = []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_def_node_enter(_node)
|
|
53
|
+
@def_depth += 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def on_def_node_leave(_node)
|
|
57
|
+
@def_depth -= 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A method body runs only when called, so only a call sent to a mixin hook's
|
|
61
|
+
# includer counts there. Each record names the class it is written in.
|
|
26
62
|
def on_call_node_enter(node)
|
|
63
|
+
return if @def_depth.positive? && !(node.receiver && in_scope?(node))
|
|
64
|
+
|
|
65
|
+
owned { record_call(node) }
|
|
66
|
+
# `base.class_eval do` in a hook runs its block on the includer, as a class body.
|
|
67
|
+
return unless @def_depth.positive? && node.block && WithOptionsScope::EVALS.include?(node.name)
|
|
68
|
+
|
|
69
|
+
@eval_depths.push([ node, @def_depth ])
|
|
70
|
+
@def_depth = 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def on_call_node_leave(node)
|
|
74
|
+
@def_depth = @eval_depths.pop.last if @eval_depths.last&.first.equal?(node)
|
|
75
|
+
@aasm = nil if @aasm && @aasm[:node].equal?(node)
|
|
76
|
+
@event = nil if @event && @event[:node].equal?(node)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# self.ignored_columns += [...] and -= [...]
|
|
80
|
+
def on_call_operator_write_node_enter(node)
|
|
81
|
+
return unless @def_depth.zero? && node.read_name == :ignored_columns && node.receiver.is_a?(Prism::SelfNode)
|
|
82
|
+
|
|
83
|
+
op = { :+ => :add, :- => :remove }[node.binary_operator]
|
|
84
|
+
owned { record_ignored_columns(node, op) } if op
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def owned
|
|
90
|
+
count = @results.size
|
|
91
|
+
yield
|
|
92
|
+
@results.drop(count).each { |result| result[:owner] = @owner_stack.dup }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def record_call(node)
|
|
96
|
+
return record_ignored_columns(node, :assign) if node.name == :ignored_columns= && node.receiver.is_a?(Prism::SelfNode)
|
|
97
|
+
return record_setting(node) if SETTINGS.include?(node.name) && node.receiver.is_a?(Prism::SelfNode)
|
|
27
98
|
return unless in_scope?(node)
|
|
99
|
+
return read_aasm(node) if @aasm
|
|
28
100
|
|
|
29
|
-
if
|
|
101
|
+
if node.name == :aasm
|
|
102
|
+
open_aasm(node) if node.block || node.arguments
|
|
103
|
+
elsif GEM_MACROS.include?(node.name)
|
|
104
|
+
record_gem_macro(node)
|
|
105
|
+
elsif node.name == :connects_to
|
|
106
|
+
# Under a condition the app may never call it, so no table is routed to its database.
|
|
107
|
+
condition = current_condition
|
|
108
|
+
@results << { macro: :connects_to, text: one_line_source(node), condition: condition,
|
|
109
|
+
writing: (writing_database(node) unless condition),
|
|
110
|
+
location: node.location.start_line, confidence: confidence_for(node) }.compact
|
|
111
|
+
elsif SIMPLE_MACROS.include?(node.name)
|
|
112
|
+
# Rails defaults the attribute to :password.
|
|
30
113
|
@results << {
|
|
31
114
|
macro: node.name,
|
|
115
|
+
attribute: (extract_symbol_args(node).first || :password).to_s,
|
|
116
|
+
written: written_options(node),
|
|
32
117
|
location: node.location.start_line,
|
|
33
118
|
confidence: confidence_for(node)
|
|
34
119
|
}
|
|
35
120
|
elsif ATTRIBUTE_MACROS.include?(node.name)
|
|
36
121
|
extract_attribute_macro(node)
|
|
122
|
+
elsif STORE_MACROS.include?(node.name)
|
|
123
|
+
extract_store(node)
|
|
37
124
|
elsif BROADCAST_MACROS.include?(node.name)
|
|
38
125
|
extract_broadcast_macro(node)
|
|
39
126
|
elsif node.name == :delegate
|
|
@@ -42,10 +129,93 @@ module RailsAiContext
|
|
|
42
129
|
extract_delegate_missing_to(node)
|
|
43
130
|
elsif node.name == :attribute
|
|
44
131
|
extract_attribute_api(node)
|
|
132
|
+
elsif node.name == :alias_attribute
|
|
133
|
+
extract_alias_attribute(node)
|
|
134
|
+
elsif node.name == :has_secure_token
|
|
135
|
+
extract_secure_token(node)
|
|
136
|
+
elsif node.name == :accepts_nested_attributes_for
|
|
137
|
+
extract_nested_attributes(node)
|
|
45
138
|
end
|
|
46
139
|
end
|
|
47
140
|
|
|
48
|
-
|
|
141
|
+
# Every keyword option as the file writes it: a lambda, `2.days` or a nested hash
|
|
142
|
+
# reads the same on every Ruby, where a value's inspect does not.
|
|
143
|
+
def written_options(node)
|
|
144
|
+
keyword_hash(node) { |value| one_line_source(value) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def record_setting(node)
|
|
148
|
+
value = node.arguments&.arguments&.first or return
|
|
149
|
+
@results << { macro: :model_setting, setting: node.name.to_s.delete_suffix("="), value: value.slice,
|
|
150
|
+
literal: setting_literal(value), location: node.location.start_line, confidence: confidence_for(node) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A symbol or string names what it spells, nil is nil; anything else is not known without running it.
|
|
154
|
+
def setting_literal(value)
|
|
155
|
+
case value
|
|
156
|
+
when Prism::SymbolNode, Prism::StringNode then value.unescaped
|
|
157
|
+
when Prism::NilNode then nil
|
|
158
|
+
else RailsAiContext::Confidence::INFERRED
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def record_gem_macro(node)
|
|
163
|
+
text = one_line_source(node, upto: node.block&.location&.start_offset)
|
|
164
|
+
text = text.gsub(/\s+/, " ").strip
|
|
165
|
+
adds = monetized_names(node) if node.name == :monetize
|
|
166
|
+
@results << { macro: :gem_macro, name: node.name, text: text, adds: adds.presence,
|
|
167
|
+
location: node.location.start_line, confidence: confidence_for(node) }.compact
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# money-rails names the attribute `as:`, or the column minus its `_cents` postfix.
|
|
171
|
+
def monetized_names(node)
|
|
172
|
+
as = extract_keyword_options(node)[:as]
|
|
173
|
+
return [ as.to_s ] if as.is_a?(Symbol) || as.is_a?(String)
|
|
174
|
+
|
|
175
|
+
extract_symbol_args(node).map(&:to_s).filter_map { |column| column.delete_suffix("_cents") if column.end_with?("_cents") }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# A named machine's column defaults to its name (AASM::Base#default_column).
|
|
179
|
+
def open_aasm(node)
|
|
180
|
+
name = extract_symbol_args(node).first
|
|
181
|
+
column = extract_keyword_options(node)[:column] || (name && name != :default ? name : "aasm_state")
|
|
182
|
+
entry = { macro: :aasm, column: column.to_s, initial: nil, states: [], events: [],
|
|
183
|
+
location: node.location.start_line, confidence: confidence_for(node) }
|
|
184
|
+
@results << entry
|
|
185
|
+
@aasm = { node: node, entry: entry } if node.block.is_a?(Prism::BlockNode)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def read_aasm(node)
|
|
189
|
+
entry = @aasm[:entry]
|
|
190
|
+
case node.name
|
|
191
|
+
when :state
|
|
192
|
+
names = extract_symbol_args(node).map(&:to_s)
|
|
193
|
+
entry[:states].concat(names)
|
|
194
|
+
# The first state is the initial one until a state says `initial: true`.
|
|
195
|
+
entry[:initial] = names.first if names.any? && (entry[:initial].nil? || extract_keyword_options(node)[:initial] == true)
|
|
196
|
+
when :event
|
|
197
|
+
name = extract_symbol_args(node).first or return
|
|
198
|
+
event = { name: name.to_s, transitions: [] }
|
|
199
|
+
entry[:events] << event
|
|
200
|
+
@event = { node: node, event: event } if node.block
|
|
201
|
+
when :transitions
|
|
202
|
+
options = extract_keyword_nodes(node)
|
|
203
|
+
from = options[:from] ? literal_strings(options[:from]) : []
|
|
204
|
+
@event[:event][:transitions] << { from: from, to: options[:to] && literal_string(options[:to]) } if @event
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def record_ignored_columns(node, op)
|
|
209
|
+
value = node.is_a?(Prism::CallNode) ? node.arguments&.arguments&.first : node.value
|
|
210
|
+
columns = value.is_a?(Prism::ArrayNode) && value.elements.all? { |e| literal_string(e) } ? literal_strings(value) : nil
|
|
211
|
+
@results << {
|
|
212
|
+
macro: :ignored_columns,
|
|
213
|
+
op: op,
|
|
214
|
+
columns: columns,
|
|
215
|
+
location: node.location.start_line,
|
|
216
|
+
confidence: columns ? RailsAiContext::Confidence::VERIFIED : RailsAiContext::Confidence::INFERRED
|
|
217
|
+
}
|
|
218
|
+
end
|
|
49
219
|
|
|
50
220
|
def extract_attribute_macro(node)
|
|
51
221
|
attrs = extract_symbol_args(node)
|
|
@@ -56,12 +226,61 @@ module RailsAiContext
|
|
|
56
226
|
macro: node.name,
|
|
57
227
|
attribute: attr_name.to_s,
|
|
58
228
|
options: options,
|
|
229
|
+
written: written_options(node),
|
|
59
230
|
location: node.location.start_line,
|
|
60
231
|
confidence: confidence_for(node)
|
|
61
232
|
}
|
|
62
233
|
end
|
|
63
234
|
end
|
|
64
235
|
|
|
236
|
+
# store :col, accessors: [...] and store_accessor :col, *keys both name
|
|
237
|
+
# the column first; the keys follow it, positionally or as accessors:.
|
|
238
|
+
def extract_store(node)
|
|
239
|
+
args = node.arguments&.arguments || []
|
|
240
|
+
column = args.first && literal_string(args.first)
|
|
241
|
+
return unless column
|
|
242
|
+
|
|
243
|
+
keys = args.drop(1).reject { |a| a.is_a?(Prism::KeywordHashNode) }.flat_map { |a| literal_strings(a) }
|
|
244
|
+
keys_node = extract_keyword_nodes(node)[:accessors]
|
|
245
|
+
keys += literal_strings(keys_node) if keys_node
|
|
246
|
+
|
|
247
|
+
@results << {
|
|
248
|
+
macro: node.name,
|
|
249
|
+
attribute: column,
|
|
250
|
+
keys: keys,
|
|
251
|
+
options: extract_keyword_options(node).slice(:prefix, :suffix),
|
|
252
|
+
location: node.location.start_line,
|
|
253
|
+
confidence: confidence_for(node)
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Rails defaults the attribute to :token.
|
|
258
|
+
def extract_secure_token(node)
|
|
259
|
+
args = node.arguments&.arguments || []
|
|
260
|
+
first = args.first
|
|
261
|
+
return if first && !first.is_a?(Prism::KeywordHashNode) && literal_string(first).nil?
|
|
262
|
+
|
|
263
|
+
@results << {
|
|
264
|
+
macro: :has_secure_token,
|
|
265
|
+
attribute: first && literal_string(first) || "token",
|
|
266
|
+
location: node.location.start_line,
|
|
267
|
+
confidence: confidence_for(node)
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def extract_nested_attributes(node)
|
|
272
|
+
names = extract_symbol_args(node).map(&:to_s)
|
|
273
|
+
return if names.empty?
|
|
274
|
+
|
|
275
|
+
@results << {
|
|
276
|
+
macro: :accepts_nested_attributes_for,
|
|
277
|
+
names: names,
|
|
278
|
+
options: extract_keyword_sources(node),
|
|
279
|
+
location: node.location.start_line,
|
|
280
|
+
confidence: confidence_for(node)
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
65
284
|
def extract_broadcast_macro(node)
|
|
66
285
|
target = extract_symbol_args(node).first
|
|
67
286
|
@results << {
|
|
@@ -97,6 +316,15 @@ module RailsAiContext
|
|
|
97
316
|
}
|
|
98
317
|
end
|
|
99
318
|
|
|
319
|
+
# The database `connects_to database: { writing: :analytics }` writes to, which names its schema dump.
|
|
320
|
+
def writing_database(node)
|
|
321
|
+
roles = extract_keyword_nodes(node)[:database]
|
|
322
|
+
return unless roles.is_a?(Prism::HashNode)
|
|
323
|
+
|
|
324
|
+
pair = roles.elements.find { |element| element.is_a?(Prism::AssocNode) && literal_string(element.key) == "writing" }
|
|
325
|
+
pair && literal_string(pair.value)
|
|
326
|
+
end
|
|
327
|
+
|
|
100
328
|
def extract_attribute_api(node)
|
|
101
329
|
args = node.arguments&.arguments || []
|
|
102
330
|
return if args.empty?
|
|
@@ -107,9 +335,12 @@ module RailsAiContext
|
|
|
107
335
|
type_arg = args[1]
|
|
108
336
|
type = case type_arg
|
|
109
337
|
when Prism::SymbolNode then type_arg.unescaped
|
|
338
|
+
when nil, Prism::KeywordHashNode then nil
|
|
339
|
+
else one_line_source(type_arg)
|
|
110
340
|
end
|
|
111
341
|
|
|
112
|
-
|
|
342
|
+
# Sources, so `default: "anon"` prints as the file writes it.
|
|
343
|
+
options = extract_keyword_sources(node).merge(extract_keyword_nodes(node).slice(:default).transform_values(&:slice))
|
|
113
344
|
|
|
114
345
|
@results << {
|
|
115
346
|
macro: :attribute,
|
|
@@ -120,6 +351,19 @@ module RailsAiContext
|
|
|
120
351
|
confidence: confidence_for(node)
|
|
121
352
|
}
|
|
122
353
|
end
|
|
354
|
+
|
|
355
|
+
def extract_alias_attribute(node)
|
|
356
|
+
new_name, old_name = extract_symbol_args(node)
|
|
357
|
+
return unless new_name && old_name
|
|
358
|
+
|
|
359
|
+
@results << {
|
|
360
|
+
macro: :alias_attribute,
|
|
361
|
+
attribute: new_name.to_s,
|
|
362
|
+
target: old_name.to_s,
|
|
363
|
+
location: node.location.start_line,
|
|
364
|
+
confidence: confidence_for(node)
|
|
365
|
+
}
|
|
366
|
+
end
|
|
123
367
|
end
|
|
124
368
|
end
|
|
125
369
|
end
|
|
@@ -27,7 +27,8 @@ module RailsAiContext
|
|
|
27
27
|
when Prism::KeywordHashNode, Prism::HashNode
|
|
28
28
|
arg.elements.each do |assoc|
|
|
29
29
|
next unless assoc.is_a?(Prism::AssocNode)
|
|
30
|
-
|
|
30
|
+
# `all: :catchall` is the :all address, written as a label.
|
|
31
|
+
pattern = assoc.key.is_a?(Prism::SymbolNode) ? ":#{assoc.key.unescaped}" : node_source(assoc.key)
|
|
31
32
|
action = extract_value(assoc.value)
|
|
32
33
|
@results << {
|
|
33
34
|
type: :routing,
|
|
@@ -6,6 +6,8 @@ module RailsAiContext
|
|
|
6
6
|
# Call sites by name, wherever they appear: inside a def, a lambda, a
|
|
7
7
|
# callback block. The macro listener only sees class-body calls.
|
|
8
8
|
class MethodCallListener < BaseListener
|
|
9
|
+
include OwnerScope
|
|
10
|
+
|
|
9
11
|
def initialize(names: nil, pattern: nil)
|
|
10
12
|
super()
|
|
11
13
|
@names = names&.map(&:to_sym)&.to_set
|
|
@@ -20,9 +22,13 @@ module RailsAiContext
|
|
|
20
22
|
name: name.to_s,
|
|
21
23
|
receiver: node.receiver&.slice,
|
|
22
24
|
line: node.location.start_line,
|
|
25
|
+
offset: node.location.start_offset,
|
|
23
26
|
arguments: extract_arg_values(node),
|
|
27
|
+
computed: computed_arguments(node),
|
|
28
|
+
constants: constant_arguments(node),
|
|
24
29
|
options: extract_keyword_sources(node),
|
|
25
|
-
snippet: node.slice.lines.first.to_s.strip
|
|
30
|
+
snippet: node.slice.lines.first.to_s.strip,
|
|
31
|
+
owner: @owner_stack.dup
|
|
26
32
|
}
|
|
27
33
|
end
|
|
28
34
|
end
|