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,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# Class-body macros that define a constructor: T::Struct's const/prop,
|
|
7
|
+
# Dry::Struct's attribute, dry-initializer's param/option and the
|
|
8
|
+
# attr_extras initializers. Each record carries the parameters the macro
|
|
9
|
+
# adds, as [kind, name, default source], and the class it is written in.
|
|
10
|
+
# Whether the macro is that library's is the consumer's call: `attribute`
|
|
11
|
+
# is also ActiveModel's.
|
|
12
|
+
class ConstructorMacroListener < GenericMacroListener
|
|
13
|
+
include OwnerScope
|
|
14
|
+
|
|
15
|
+
STRUCT_MACROS = %i[const prop attribute attribute?].freeze
|
|
16
|
+
DRY_INITIALIZER_MACROS = %i[param option].freeze
|
|
17
|
+
ATTR_EXTRAS_MACROS = %i[
|
|
18
|
+
attr_initialize pattr_initialize vattr_initialize rattr_initialize aattr_initialize
|
|
19
|
+
attr_private_initialize attr_value_initialize attr_reader_initialize attr_accessor_initialize
|
|
20
|
+
method_object static_facade
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
super(STRUCT_MACROS + DRY_INITIALIZER_MACROS + ATTR_EXTRAS_MACROS + %i[extend])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_call_node_enter(node)
|
|
28
|
+
count = @results.size
|
|
29
|
+
super
|
|
30
|
+
@results[count..].each do |record|
|
|
31
|
+
record[:owner] = @owner_stack.dup
|
|
32
|
+
record[:source] = one_line_source(node)
|
|
33
|
+
record[:params] = params_for(node)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def params_for(node)
|
|
40
|
+
args = Array(node.arguments&.arguments)
|
|
41
|
+
case node.name
|
|
42
|
+
when :const, :prop then struct_param(args, extract_keyword_nodes(node))
|
|
43
|
+
when :attribute, :attribute? then dry_struct_param(node.name, args)
|
|
44
|
+
when :param, :option then dry_initializer_param(node.name, args, extract_keyword_nodes(node))
|
|
45
|
+
when :extend then []
|
|
46
|
+
else attr_extras_params(node.name == :static_facade ? args.drop(1) : args)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A T::Struct prop is optional with a default, a factory, or a nilable type.
|
|
51
|
+
def struct_param(args, options)
|
|
52
|
+
name = literal_string(args.first) or return []
|
|
53
|
+
default = options[:default] || options[:factory]
|
|
54
|
+
return [ [ :key, name, default_text(default) ] ] if default
|
|
55
|
+
return [ [ :key, name, "nil" ] ] if args[1] && !args[1].is_a?(Prism::KeywordHashNode) && args[1].slice.start_with?("T.nilable(")
|
|
56
|
+
|
|
57
|
+
[ [ :keyreq, name, nil ] ]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dry_struct_param(macro, args)
|
|
61
|
+
name = literal_string(args.first) or return []
|
|
62
|
+
return [ [ :key, name, "nil" ] ] if macro == :attribute?
|
|
63
|
+
|
|
64
|
+
default = type_default(args[1])
|
|
65
|
+
default ? [ [ :key, name, default ] ] : [ [ :keyreq, name, nil ] ]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# `Types::String.default("USD")`, anywhere in the type's call chain.
|
|
69
|
+
def type_default(node)
|
|
70
|
+
while node.is_a?(Prism::CallNode)
|
|
71
|
+
return default_text(node.arguments&.arguments&.first) || "..." if node.name == :default
|
|
72
|
+
|
|
73
|
+
node = node.receiver
|
|
74
|
+
end
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dry_initializer_param(macro, args, options)
|
|
79
|
+
name = literal_string(args.first) or return []
|
|
80
|
+
default = (default_text(options[:default]) if options[:default]) ||
|
|
81
|
+
("nil" if options[:optional].is_a?(Prism::TrueNode))
|
|
82
|
+
positional = macro == :param
|
|
83
|
+
kind = positional ? (default ? :opt : :req) : (default ? :key : :keyreq)
|
|
84
|
+
[ [ kind, name, default ] ]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Symbols are positional; an array holds keywords, `name!` required.
|
|
88
|
+
def attr_extras_params(args)
|
|
89
|
+
args.flat_map do |arg|
|
|
90
|
+
if arg.is_a?(Prism::ArrayNode)
|
|
91
|
+
arg.elements.flat_map { |element| attr_extras_keywords(element) }
|
|
92
|
+
else
|
|
93
|
+
name = literal_string(arg)
|
|
94
|
+
name ? [ [ :req, name, nil ] ] : []
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attr_extras_keywords(element)
|
|
100
|
+
if element.is_a?(Prism::KeywordHashNode) || element.is_a?(Prism::HashNode)
|
|
101
|
+
return element.elements.grep(Prism::AssocNode).filter_map do |assoc|
|
|
102
|
+
key = literal_string(assoc.key)
|
|
103
|
+
[ :key, key, assoc.value.slice ] if key
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
name = literal_string(element) or return []
|
|
108
|
+
name.end_with?("!") ? [ [ :keyreq, name.delete_suffix("!"), nil ] ] : [ [ :key, name, "nil" ] ]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A lambda default reads as the value it returns.
|
|
112
|
+
def default_text(node)
|
|
113
|
+
return nil unless node
|
|
114
|
+
|
|
115
|
+
body = node.body if node.is_a?(Prism::LambdaNode)
|
|
116
|
+
body = node.block.body if node.is_a?(Prism::CallNode) && %i[lambda proc].include?(node.name) && node.block.is_a?(Prism::BlockNode)
|
|
117
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : []
|
|
118
|
+
statements.size == 1 ? statements.first.slice : node.slice
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# Where a test helper points factory_bot, literal paths only:
|
|
7
|
+
#
|
|
8
|
+
# FactoryBot.definition_file_paths = %w[custom/factories] → { replace: true, paths: ["custom/factories"] }
|
|
9
|
+
# FactoryBot.definition_file_paths << "lib/factories" → { replace: false, paths: ["lib/factories"] }
|
|
10
|
+
# FactoryBot.find_definitions / FactoryBot.reload → { load: :find_definitions } / { load: :reload }
|
|
11
|
+
#
|
|
12
|
+
# The path forms are LiteralPaths'; a write with any path it cannot read records `unread: true`.
|
|
13
|
+
class DefinitionFilePathsListener < BaseListener
|
|
14
|
+
include LiteralPaths
|
|
15
|
+
|
|
16
|
+
LOADS = %i[find_definitions reload].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(file: nil)
|
|
19
|
+
super()
|
|
20
|
+
@file = file
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_call_node_enter(node)
|
|
24
|
+
if LOADS.include?(node.name) && factory_bot?(node.receiver)
|
|
25
|
+
@results << { load: node.name }
|
|
26
|
+
elsif node.name == :definition_file_paths= && factory_bot?(node.receiver)
|
|
27
|
+
record(true) { collect_arguments(node) }
|
|
28
|
+
elsif APPENDING.include?(node.name) && setting?(node.receiver)
|
|
29
|
+
record(false) { collect_arguments(node) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_call_operator_write_node_enter(node)
|
|
34
|
+
return unless node.read_name == :definition_file_paths && node.binary_operator == :+ && factory_bot?(node.receiver)
|
|
35
|
+
|
|
36
|
+
record(false) { collect_paths(node.value) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def collect_arguments(node)
|
|
42
|
+
Array(node.arguments&.arguments).map { |argument| collect_paths(argument) }.all?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def record(replace)
|
|
46
|
+
before = @results.size
|
|
47
|
+
read = yield
|
|
48
|
+
entry = { replace: replace, paths: @results.slice!(before..) }
|
|
49
|
+
@results << (read ? entry : entry.merge(unread: true))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def setting?(node)
|
|
53
|
+
node.is_a?(Prism::CallNode) && node.name == :definition_file_paths && factory_bot?(node.receiver)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def factory_bot?(node)
|
|
57
|
+
case node
|
|
58
|
+
when Prism::ConstantReadNode then node.name == :FactoryBot
|
|
59
|
+
when Prism::ConstantPathNode then node.parent.nil? && node.name == :FactoryBot
|
|
60
|
+
else false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -59,7 +59,7 @@ module RailsAiContext
|
|
|
59
59
|
|
|
60
60
|
@results << {
|
|
61
61
|
name: key.to_s,
|
|
62
|
-
values: enum_values(assoc.value),
|
|
62
|
+
values: normalize_enum_values(enum_values(assoc.value)),
|
|
63
63
|
options: options,
|
|
64
64
|
location: node.location.start_line,
|
|
65
65
|
confidence: confidence_for(node)
|
|
@@ -104,7 +104,7 @@ module RailsAiContext
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def enum_modifier_key?(key)
|
|
107
|
-
%i[prefix suffix _prefix _suffix default scopes].include?(key)
|
|
107
|
+
%i[prefix suffix _prefix _suffix default _default scopes _scopes].include?(key)
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
end
|
|
@@ -7,6 +7,7 @@ module RailsAiContext
|
|
|
7
7
|
# ENV["KEY"], ENV.fetch("KEY"), ENV.fetch("KEY", "default")
|
|
8
8
|
class EnvAccessListener < BaseListener
|
|
9
9
|
def on_call_node_enter(node)
|
|
10
|
+
return extract_creds(node) if creds_receiver?(node.receiver)
|
|
10
11
|
return unless env_receiver?(node.receiver)
|
|
11
12
|
|
|
12
13
|
case node.name
|
|
@@ -19,6 +20,36 @@ module RailsAiContext
|
|
|
19
20
|
|
|
20
21
|
private
|
|
21
22
|
|
|
23
|
+
# Rails 8.2's `Rails.app.creds` checks ENV before the encrypted
|
|
24
|
+
# credentials, and `Rails.app.envs` reads only ENV.
|
|
25
|
+
def creds_receiver?(receiver)
|
|
26
|
+
return false unless receiver.is_a?(Prism::CallNode) && %i[creds envs].include?(receiver.name)
|
|
27
|
+
|
|
28
|
+
app = receiver.receiver
|
|
29
|
+
app.is_a?(Prism::CallNode) && %i[app application].include?(app.name) &&
|
|
30
|
+
app.receiver.is_a?(Prism::ConstantReadNode) && app.receiver.name == :Rails
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# `require(:database, :host)` reads ENV["DATABASE__HOST"].
|
|
34
|
+
def extract_creds(node)
|
|
35
|
+
return unless %i[require option].include?(node.name)
|
|
36
|
+
|
|
37
|
+
args = node.arguments&.arguments || []
|
|
38
|
+
parts = args.reject { |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
39
|
+
keys = parts.map { |arg| literal_string(arg) }
|
|
40
|
+
return if keys.empty? || keys.any?(&:nil?)
|
|
41
|
+
|
|
42
|
+
default = extract_keyword_nodes(node)[:default]
|
|
43
|
+
optional = node.name == :option
|
|
44
|
+
@results << {
|
|
45
|
+
method: optional && default.nil? ? "[]" : "fetch",
|
|
46
|
+
key: keys.map(&:upcase).join("__"),
|
|
47
|
+
has_default: !default.nil?,
|
|
48
|
+
default: literal_value(default),
|
|
49
|
+
location: node.location.start_line
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
22
53
|
def env_receiver?(receiver)
|
|
23
54
|
receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :ENV
|
|
24
55
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# ConditionalMacroListener plus `callbacks`: what each positional argument of a filter macro
|
|
7
|
+
# adds, named at the node as the booted tier names the callback it reflects.
|
|
8
|
+
class FilterMacroListener < ConditionalMacroListener
|
|
9
|
+
def on_call_node_enter(node)
|
|
10
|
+
count = @results.size
|
|
11
|
+
super
|
|
12
|
+
@results.last[:callbacks] = callbacks(node) if @results.size > count
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# [:name, "x"] for a symbol, string or class; [:object, "Klass"] for an instance;
|
|
18
|
+
# [:lambda] for `-> {}` or `lambda {}`, [:block] for a proc; [:unread, source] for anything else.
|
|
19
|
+
def callbacks(node)
|
|
20
|
+
Array(node.arguments&.arguments).reject { |arg| arg.is_a?(Prism::KeywordHashNode) }.map do |arg|
|
|
21
|
+
if (text = literal_string(arg)) then [ :name, text ]
|
|
22
|
+
elsif proc_argument?(arg) then [ arg.is_a?(Prism::LambdaNode) || arg.name == :lambda ? :lambda : :block ]
|
|
23
|
+
elsif (const = constant_name(arg)) then [ :name, const ]
|
|
24
|
+
elsif (klass = object_name(arg)) then [ :object, klass ]
|
|
25
|
+
else [ :unread, one_line_source(arg) ]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The class the booted tier names an object filter by: `Class` for an anonymous class,
|
|
31
|
+
# an instance's nearest named class (`Class.new(Base) {}.new` is Base's, `Class.new {}.new` Object's).
|
|
32
|
+
def object_name(node)
|
|
33
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :new && node.receiver
|
|
34
|
+
return "Class" if %w[Class Struct].include?(constant_name(node.receiver))
|
|
35
|
+
|
|
36
|
+
named_class(node.receiver)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def named_class(node)
|
|
40
|
+
return constant_name(node) unless node.is_a?(Prism::CallNode) && node.name == :new
|
|
41
|
+
|
|
42
|
+
case constant_name(node.receiver)
|
|
43
|
+
when "Class" then (superclass = node.arguments&.arguments&.first) ? named_class(superclass) : "Object"
|
|
44
|
+
when "Struct" then "Struct"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def constant_name(node)
|
|
49
|
+
constant_path_string(node) if node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# Fixture directories a test helper sets, literal paths only:
|
|
7
|
+
#
|
|
8
|
+
# config.fixture_paths = [Rails.root.join("spec/fixtures")]
|
|
9
|
+
# self.fixture_paths << "#{Rails.root}/test/fixtures/"
|
|
10
|
+
# self.fixture_path = "#{::Rails.root}/spec/fixtures"
|
|
11
|
+
# ActiveSupport::TestCase.fixture_paths << File.expand_path("../extra_fx", __dir__)
|
|
12
|
+
#
|
|
13
|
+
# A path built from __dir__ or __FILE__ is read only when the walk is given the helper's
|
|
14
|
+
# `file`, relative to the app root.
|
|
15
|
+
# The path forms are LiteralPaths'. A write with any path it cannot read records :unread,
|
|
16
|
+
# so a caller still knows the setting is set.
|
|
17
|
+
class FixturePathsListener < BaseListener
|
|
18
|
+
include LiteralPaths
|
|
19
|
+
|
|
20
|
+
SETTINGS = %i[fixture_paths fixture_path].to_set.freeze
|
|
21
|
+
ASSIGNING = %i[fixture_paths= fixture_path=].to_set.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(file: nil)
|
|
24
|
+
super()
|
|
25
|
+
@file = file
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_call_node_enter(node)
|
|
29
|
+
if ASSIGNING.include?(node.name)
|
|
30
|
+
return unless setting_receiver?(node.receiver)
|
|
31
|
+
else
|
|
32
|
+
return unless APPENDING.include?(node.name) && fixture_setting?(node.receiver)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
record_write { Array(node.arguments&.arguments).map { |argument| collect_paths(argument) }.all? }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_call_operator_write_node_enter(node)
|
|
39
|
+
return unless SETTINGS.include?(node.read_name) && node.binary_operator == :+ && setting_receiver?(node.receiver)
|
|
40
|
+
|
|
41
|
+
record_write { collect_paths(node.value) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def record_write
|
|
47
|
+
@results << :unread unless yield
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fixture_setting?(node)
|
|
51
|
+
node.is_a?(Prism::CallNode) && SETTINGS.include?(node.name) && setting_receiver?(node.receiver)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The test case class itself, named or as self, or RSpec's `config` (the block parameter `RSpec.configure` yields).
|
|
55
|
+
def setting_receiver?(node)
|
|
56
|
+
node.nil? || node.is_a?(Prism::SelfNode) || node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) ||
|
|
57
|
+
((node.is_a?(Prism::CallNode) || node.is_a?(Prism::LocalVariableReadNode)) && node.name == :config)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -6,6 +6,7 @@ module RailsAiContext
|
|
|
6
6
|
# Detects Gemfile DSL patterns via Prism AST:
|
|
7
7
|
# gem "name", "version", options...
|
|
8
8
|
# group :development, :test do ... end
|
|
9
|
+
# ruby "3.3.6", and an :unknown_gems entry for each call that adds gems the file does not name.
|
|
9
10
|
class GemfileDslListener < BaseListener
|
|
10
11
|
def initialize
|
|
11
12
|
super
|
|
@@ -20,6 +21,12 @@ module RailsAiContext
|
|
|
20
21
|
extract_gem(node)
|
|
21
22
|
when :source
|
|
22
23
|
extract_source(node)
|
|
24
|
+
when :eval_gemfile
|
|
25
|
+
extract_eval_gemfile(node)
|
|
26
|
+
when :ruby
|
|
27
|
+
extract_ruby(node)
|
|
28
|
+
when :gemspec
|
|
29
|
+
unknown_gems(node)
|
|
23
30
|
when :group
|
|
24
31
|
return unless node.block
|
|
25
32
|
|
|
@@ -47,7 +54,7 @@ module RailsAiContext
|
|
|
47
54
|
return if args.empty?
|
|
48
55
|
|
|
49
56
|
name_arg = args.first
|
|
50
|
-
return unless name_arg.is_a?(Prism::StringNode)
|
|
57
|
+
return unknown_gems(node) unless name_arg.is_a?(Prism::StringNode)
|
|
51
58
|
|
|
52
59
|
name = name_arg.unescaped
|
|
53
60
|
version = nil
|
|
@@ -78,6 +85,40 @@ module RailsAiContext
|
|
|
78
85
|
}
|
|
79
86
|
end
|
|
80
87
|
|
|
88
|
+
# `eval_gemfile "Gemfile.local"`, or `File.expand_path("x", __dir__)`;
|
|
89
|
+
# Bundler reads the path from the evaluating file's directory.
|
|
90
|
+
def extract_eval_gemfile(node)
|
|
91
|
+
arg = node.arguments&.arguments&.first
|
|
92
|
+
if arg.is_a?(Prism::CallNode) && arg.name == :expand_path && file_constant?(arg.receiver)
|
|
93
|
+
inner = arg.arguments&.arguments || []
|
|
94
|
+
arg = inner.first if inner.size == 2 && dir_call?(inner.last)
|
|
95
|
+
end
|
|
96
|
+
return unknown_gems(node) unless arg.is_a?(Prism::StringNode)
|
|
97
|
+
|
|
98
|
+
@results << {
|
|
99
|
+
type: :eval_gemfile,
|
|
100
|
+
path: arg.unescaped,
|
|
101
|
+
groups: @current_groups.flatten.uniq,
|
|
102
|
+
location: node.location.start_line
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Bundler takes `engine:` and `engine_version:` beside the version.
|
|
107
|
+
def extract_ruby(node)
|
|
108
|
+
options = extract_keyword_nodes(node)
|
|
109
|
+
@results << {
|
|
110
|
+
type: :ruby,
|
|
111
|
+
version: literal_string(node.arguments&.arguments&.first),
|
|
112
|
+
engine: literal_string(options[:engine]),
|
|
113
|
+
engine_version: literal_string(options[:engine_version]),
|
|
114
|
+
location: node.location.start_line
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def unknown_gems(node)
|
|
119
|
+
@results << { type: :unknown_gems, call: node.name, location: node.location.start_line }
|
|
120
|
+
end
|
|
121
|
+
|
|
81
122
|
def extract_source(node)
|
|
82
123
|
args = node.arguments&.arguments || []
|
|
83
124
|
url_arg = args.first
|
|
@@ -4,14 +4,18 @@ module RailsAiContext
|
|
|
4
4
|
module Introspectors
|
|
5
5
|
module Listeners
|
|
6
6
|
class GenericMacroListener < BaseListener
|
|
7
|
-
|
|
7
|
+
# `any_receiver` also counts `base.macro`, the shape a mixin's `self.included(base)` hook writes.
|
|
8
|
+
def initialize(*target_methods, block_source: [], call_source: [], any_receiver: false)
|
|
8
9
|
super()
|
|
10
|
+
@any_receiver = any_receiver
|
|
9
11
|
@target_methods = target_methods.flatten.map(&:to_sym).to_set
|
|
10
12
|
@enclosing = []
|
|
13
|
+
@block_source = block_source.to_set
|
|
14
|
+
@call_source = call_source.to_set
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def on_call_node_enter(node)
|
|
14
|
-
return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
18
|
+
return unless @any_receiver || node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
15
19
|
return unless @target_methods.include?(node.name)
|
|
16
20
|
|
|
17
21
|
@results << {
|
|
@@ -21,6 +25,9 @@ module RailsAiContext
|
|
|
21
25
|
options: extract_keyword_options(node),
|
|
22
26
|
option_values: extract_keyword_sources(node),
|
|
23
27
|
option_nodes: extract_keyword_nodes(node),
|
|
28
|
+
block: (one_line_source(node.block) if node.block.is_a?(Prism::BlockNode) && @block_source.include?(node.name)),
|
|
29
|
+
# Where a block or a lambda argument opens: the line Proc#source_location gives.
|
|
30
|
+
proc_lines: proc_lines(node),
|
|
24
31
|
# Offsets, not line numbers: a one-line block puts the parent and
|
|
25
32
|
# its nested calls on one line, and a consumer pairing them by
|
|
26
33
|
# line then attaches the second child to the first.
|
|
@@ -30,6 +37,9 @@ module RailsAiContext
|
|
|
30
37
|
location: node.location.start_line,
|
|
31
38
|
confidence: confidence_for(node)
|
|
32
39
|
}
|
|
40
|
+
@results.last[:text] = one_line_source(node) if @call_source.include?(node.name)
|
|
41
|
+
condition = macro_condition
|
|
42
|
+
@results.last[:condition] = condition if condition
|
|
33
43
|
|
|
34
44
|
# A target macro that takes a block encloses whatever the block
|
|
35
45
|
# declares: `string :title` inside `hash :order_params do ... end`
|
|
@@ -40,6 +50,22 @@ module RailsAiContext
|
|
|
40
50
|
def on_call_node_leave(node)
|
|
41
51
|
@enclosing.pop if @enclosing.last.equal?(node)
|
|
42
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def macro_condition; end
|
|
57
|
+
|
|
58
|
+
# A def evaluates to its name, which is what `helper_method def foo` passes.
|
|
59
|
+
def extract_symbol_args(node)
|
|
60
|
+
Array(node.arguments&.arguments).filter_map { |arg| arg.is_a?(Prism::DefNode) ? arg.name : literal_string(arg)&.to_sym }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A call's line is where it opens, as Proc#source_location gives it.
|
|
64
|
+
def proc_lines(node)
|
|
65
|
+
procs = Array(node.arguments&.arguments).select { |arg| proc_argument?(arg) }
|
|
66
|
+
procs << node.block if node.block.is_a?(Prism::BlockNode)
|
|
67
|
+
procs.map { |found| found.location.start_line }
|
|
68
|
+
end
|
|
43
69
|
end
|
|
44
70
|
end
|
|
45
71
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# What a Grape API class body declares: its superclass, version and
|
|
7
|
+
# prefix, each endpoint with the namespaces around it and the params
|
|
8
|
+
# block before it, and the APIs it mounts. Whether a class is a Grape API
|
|
9
|
+
# is the consumer's call, since a superclass can be another API class.
|
|
10
|
+
class GrapeApiListener < BaseListener
|
|
11
|
+
VERBS = %i[get post put patch delete head options query].freeze
|
|
12
|
+
NAMESPACES = %i[namespace group resource resources segment route_param].freeze
|
|
13
|
+
PARAMS = %i[requires optional].freeze
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super
|
|
17
|
+
@scopes = [ nil ]
|
|
18
|
+
@blocks = []
|
|
19
|
+
@pending = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_class_node_enter(node)
|
|
23
|
+
name = scope_name(node)
|
|
24
|
+
superclass = node.superclass && (node.superclass.is_a?(Prism::ConstantReadNode) || node.superclass.is_a?(Prism::ConstantPathNode)) ? constant_path_string(node.superclass) : nil
|
|
25
|
+
@results << { kind: :class, owner: name, superclass: superclass }
|
|
26
|
+
@scopes.push(name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_class_node_leave(_node)
|
|
30
|
+
@scopes.pop
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_module_node_enter(node)
|
|
34
|
+
@scopes.push(scope_name(node))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def on_module_node_leave(_node)
|
|
38
|
+
@scopes.pop
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def on_call_node_enter(node)
|
|
42
|
+
frame = frame_for(node)
|
|
43
|
+
@blocks.push(frame) if node.block.is_a?(Prism::BlockNode)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_call_node_leave(node)
|
|
47
|
+
return unless node.block.is_a?(Prism::BlockNode)
|
|
48
|
+
|
|
49
|
+
@pending = [] if @blocks.pop.first == :namespace
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Records what the call declares and returns the frame its block opens.
|
|
55
|
+
def frame_for(node)
|
|
56
|
+
owner = @scopes.last
|
|
57
|
+
return [ :body ] if owner.nil? || node.receiver || in_endpoint?
|
|
58
|
+
|
|
59
|
+
args = Array(node.arguments&.arguments)
|
|
60
|
+
case node.name
|
|
61
|
+
when *VERBS
|
|
62
|
+
@results << { kind: :endpoint, owner: owner, verb: node.name.to_s.upcase, path: literal_string(args.first) || "",
|
|
63
|
+
namespace: namespace, params: inherited_params + @pending }
|
|
64
|
+
@pending = []
|
|
65
|
+
[ :endpoint ]
|
|
66
|
+
when *NAMESPACES
|
|
67
|
+
space = literal_string(args.first)
|
|
68
|
+
if node.name == :route_param && space
|
|
69
|
+
type = keyword(node, :type)
|
|
70
|
+
@pending += [ { name: space, type: type, required: true } ] if type
|
|
71
|
+
space = ":#{space}"
|
|
72
|
+
end
|
|
73
|
+
# Grape declares these on the setting the namespace inherits, so every endpoint inside gets them.
|
|
74
|
+
params, @pending = @pending, []
|
|
75
|
+
[ :namespace, space, params ]
|
|
76
|
+
when :params
|
|
77
|
+
[ :params ]
|
|
78
|
+
when *PARAMS
|
|
79
|
+
if @blocks.last&.first == :params
|
|
80
|
+
type = keyword(node, :type)
|
|
81
|
+
args.filter_map { |arg| literal_string(arg) }.each do |name|
|
|
82
|
+
@pending << { name: name, type: type, required: node.name == :requires }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
[ :body ]
|
|
86
|
+
when :version, :prefix
|
|
87
|
+
value = literal_string(args.first)
|
|
88
|
+
@results << { kind: node.name, owner: owner, value: value, using: keyword(node, :using) || "path" } if value
|
|
89
|
+
[ :body ]
|
|
90
|
+
when :mount
|
|
91
|
+
mounts(args).each { |target, path| @results << { kind: :mount, owner: owner, target: target, path: path, namespace: namespace } }
|
|
92
|
+
[ :body ]
|
|
93
|
+
else
|
|
94
|
+
[ :body ]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def in_endpoint?
|
|
99
|
+
@blocks.any? { |frame| frame.first == :endpoint }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def namespace
|
|
103
|
+
@blocks.filter_map { |kind, space| space if kind == :namespace }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def inherited_params
|
|
107
|
+
@blocks.flat_map { |kind, _, params| kind == :namespace ? params : [] }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def keyword(node, key)
|
|
111
|
+
value = extract_keyword_nodes(node)[key]
|
|
112
|
+
value && (literal_string(value) || value.slice)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `mount V1::Users`, `mount V1::Users => "/v1"` and the braced form.
|
|
116
|
+
def mounts(args)
|
|
117
|
+
args.flat_map do |arg|
|
|
118
|
+
case arg
|
|
119
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then [ [ constant_path_string(arg), "/" ] ]
|
|
120
|
+
when Prism::KeywordHashNode, Prism::HashNode
|
|
121
|
+
arg.elements.grep(Prism::AssocNode).filter_map do |assoc|
|
|
122
|
+
next unless assoc.key.is_a?(Prism::ConstantReadNode) || assoc.key.is_a?(Prism::ConstantPathNode)
|
|
123
|
+
|
|
124
|
+
[ constant_path_string(assoc.key), literal_string(assoc.value) || "/" ]
|
|
125
|
+
end
|
|
126
|
+
else []
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def scope_name(node)
|
|
132
|
+
nested_name(node.constant_path, @scopes.last)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|