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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
|
+
require "pathname"
|
|
4
5
|
require_relative "safe_file"
|
|
6
|
+
require_relative "safe_path"
|
|
7
|
+
require_relative "polyfill/data"
|
|
5
8
|
|
|
6
9
|
module RailsAiContext
|
|
7
10
|
# Which gems an app resolved, read once per lockfile, and the one answer
|
|
@@ -15,28 +18,50 @@ module RailsAiContext
|
|
|
15
18
|
MAX_SIZE = 5 * 1024 * 1024
|
|
16
19
|
SPEC_LINE = /\A {4}(\S+) \(([^)]+)\)\s*\z/
|
|
17
20
|
DEPENDENCY_LINE = /\A {2}(\S+?)!?(?: \(.*\))?\s*\z/
|
|
18
|
-
|
|
21
|
+
# Bundler writes a non-CRuby engine after the version: "ruby 3.1.4p0 (jruby 9.4.8.0)".
|
|
22
|
+
RUBY_LINE = /\A\s+ruby (\S+)(?: \((\S+) ([^)]+)\))?/
|
|
19
23
|
REMOTE_LINE = /\A {2}remote: (.+?)\s*\z/
|
|
20
|
-
GEMFILE_RUBY_LINE = /^\s*ruby\s+(["'])([^"']+)\1/
|
|
21
24
|
PLAIN_VERSION = /\A\d+(?:\.\d+)*\S*\z/
|
|
22
25
|
TOOL_VERSIONS_RUBY = /^ruby[ \t]+(\S+)/
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
# Version managers write another engine as "<engine>-<version>", as ruby-build names it.
|
|
27
|
+
ENGINE_PREFIXED = /\A([a-z][a-z+]*)-(\d\S*)\z/
|
|
28
|
+
ENGINE_NAMES = {
|
|
29
|
+
"jruby" => "JRuby", "truffleruby" => "TruffleRuby", "truffleruby+graalvm" => "TruffleRuby+GraalVM",
|
|
30
|
+
"mruby" => "mruby", "rbx" => "Rubinius"
|
|
31
|
+
}.freeze
|
|
32
|
+
# mise's project files, highest precedence first (mise docs, configuration).
|
|
33
|
+
MISE_FILES = [ "mise.local.toml", "mise.toml", ".mise.toml", "mise/config.toml", ".mise/config.toml", ".config/mise.toml",
|
|
34
|
+
".config/mise/config.toml" ].freeze
|
|
35
|
+
VERSION_FILES = [ ".ruby-version", ".tool-versions", *MISE_FILES ].freeze
|
|
36
|
+
MISE_TOOLS = /^[ \t]*\[tools\][ \t]*$(.*?)(?=^[ \t]*\[|\z)/m
|
|
37
|
+
# ruby = "3.3.6", ruby = ["3.3.6", ...] or ruby = { version = "3.3.6" }
|
|
38
|
+
MISE_RUBY = /^[ \t]*["']?ruby["']?[ \t]*=[ \t]*(?:\[[ \t]*|\{[^}\n]*?version[ \t]*=[ \t]*)?["']([^"'\n]+)["']/
|
|
26
39
|
|
|
27
40
|
class Spec
|
|
28
|
-
# `
|
|
29
|
-
attr_reader :ruby_versions, :reason, :path_remotes
|
|
41
|
+
# `ruby_engine` is a non-CRuby engine with its own version ("JRuby 9.4.8.0"); `outside_gemfile` is never read.
|
|
42
|
+
attr_reader :ruby_versions, :ruby_engine, :reason, :path_remotes, :outside_gemfile
|
|
43
|
+
# With no lockfile, the gems the Gemfile names; nil when it names only some of them.
|
|
44
|
+
attr_reader :gemfile_gems
|
|
30
45
|
|
|
31
|
-
def initialize(versions, ruby_versions: {}, reason: nil, absent: false, direct: nil, path_remotes: []
|
|
46
|
+
def initialize(versions, ruby_versions: {}, ruby_engine: nil, reason: nil, absent: false, direct: nil, path_remotes: [],
|
|
47
|
+
outside_gemfile: nil, no_repo: false, gemfile_gems: nil)
|
|
32
48
|
@versions = versions
|
|
49
|
+
@gemfile_gems = gemfile_gems
|
|
50
|
+
@outside_gemfile = outside_gemfile
|
|
51
|
+
@no_repo = no_repo
|
|
33
52
|
@path_remotes = path_remotes
|
|
34
53
|
@ruby_versions = ruby_versions
|
|
54
|
+
@ruby_engine = ruby_engine
|
|
35
55
|
@reason = reason
|
|
36
56
|
@absent = absent
|
|
37
57
|
@direct = direct || Set.new
|
|
38
58
|
end
|
|
39
59
|
|
|
60
|
+
# Why the bundle config/boot.rb names went unread, worded the same by every tool.
|
|
61
|
+
def unread_bundle
|
|
62
|
+
GemLock.unread_bundle(@outside_gemfile, @no_repo) if @outside_gemfile
|
|
63
|
+
end
|
|
64
|
+
|
|
40
65
|
# Several files may name a version and they often disagree, so the source is kept too.
|
|
41
66
|
def ruby_version
|
|
42
67
|
@ruby_versions.values.first
|
|
@@ -85,46 +110,161 @@ module RailsAiContext
|
|
|
85
110
|
end
|
|
86
111
|
end
|
|
87
112
|
|
|
113
|
+
# `dir` holds the Gemfile and lockfile; `trusted` is the tree a path gem of theirs must stay
|
|
114
|
+
# inside; `outside` names a bundle config/boot.rb points at that is never read, `no_repo` says no git repository holds the app.
|
|
115
|
+
Bundle = Data.define(:lockfile, :gemfile, :lock_label, :gemfile_label, :dir, :trusted, :outside, :no_repo)
|
|
116
|
+
|
|
88
117
|
MUTEX = Mutex.new
|
|
89
118
|
CACHE = {}
|
|
90
119
|
private_constant :MUTEX, :CACHE
|
|
91
120
|
|
|
92
121
|
module_function
|
|
93
122
|
|
|
123
|
+
# Bundler looks for gems.rb before Gemfile.
|
|
124
|
+
def gemfile_name(root)
|
|
125
|
+
File.file?(File.join(root.to_s, "gems.rb")) ? "gems.rb" : "Gemfile"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# gems.rb locks to gems.locked, Gemfile to Gemfile.lock.
|
|
129
|
+
def lockfile_name(root)
|
|
130
|
+
gemfile_name(root) == "gems.rb" ? "gems.locked" : "Gemfile.lock"
|
|
131
|
+
end
|
|
132
|
+
|
|
94
133
|
def for(root)
|
|
95
134
|
root = root.to_s
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
bundle = bundle(root)
|
|
136
|
+
stamp = [ bundle.lockfile, bundle.gemfile, File.join(root, "config/boot.rb"),
|
|
137
|
+
*VERSION_FILES.map { |name| File.join(root, name) } ].map { |file| file && mtime(file) } << loaded?
|
|
138
|
+
stamp << Introspectors::GemfileGems.stamps(bundle) if loaded?
|
|
99
139
|
|
|
100
140
|
MUTEX.synchronize do
|
|
101
|
-
cached = CACHE[
|
|
141
|
+
cached = CACHE[root]
|
|
102
142
|
return cached[:spec] if cached && cached[:stamp] == stamp
|
|
103
143
|
|
|
104
144
|
# Which gems resolved and which Ruby the app declares are two facts,
|
|
105
145
|
# and the Gemfile answers the second whether or not a lockfile answers
|
|
106
146
|
# the first.
|
|
107
147
|
spec = if stamp.first
|
|
108
|
-
parse(
|
|
148
|
+
parse(bundle, root)
|
|
109
149
|
else
|
|
110
|
-
|
|
111
|
-
reason: "No Gemfile.lock found", absent: true)
|
|
150
|
+
absent_spec(root, bundle)
|
|
112
151
|
end
|
|
113
|
-
CACHE[
|
|
152
|
+
CACHE[root] = { stamp: stamp, spec: spec }
|
|
114
153
|
spec
|
|
115
154
|
end
|
|
116
155
|
end
|
|
117
156
|
|
|
157
|
+
# The app's own Gemfile and lockfile, or, with no lockfile of its own, the
|
|
158
|
+
# bundle config/boot.rb points Bundler at (an engine's test/dummy).
|
|
159
|
+
def bundle(root)
|
|
160
|
+
root = root.to_s
|
|
161
|
+
own = Bundle.new(lockfile: File.join(root, lockfile_name(root)), gemfile: File.join(root, gemfile_name(root)),
|
|
162
|
+
lock_label: lockfile_name(root), gemfile_label: gemfile_name(root), dir: root, trusted: root, outside: nil, no_repo: false)
|
|
163
|
+
return own if File.file?(own.lockfile)
|
|
164
|
+
|
|
165
|
+
boot_bundle(root, own) || own
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Read only inside the app's git repository, the trust its own Gemfile.lock has; anything else is named and left unread.
|
|
169
|
+
def boot_bundle(root, own)
|
|
170
|
+
target = boot_gemfile(root)
|
|
171
|
+
return nil unless target
|
|
172
|
+
|
|
173
|
+
real_root = File.realpath(root)
|
|
174
|
+
dir = File.dirname(target)
|
|
175
|
+
label = ->(file) { Pathname.new(File.join(dir, file)).relative_path_from(Pathname.new(real_root)).to_s }
|
|
176
|
+
gemfile = File.basename(target)
|
|
177
|
+
lockfile = gemfile == "gems.rb" ? "gems.locked" : "#{gemfile}.lock"
|
|
178
|
+
repo = SafePath.git_root(real_root)
|
|
179
|
+
unless repo && File.directory?(dir) && SafePath.contained?(File.realpath(dir), repo)
|
|
180
|
+
return own.with(lockfile: nil, outside: label.(gemfile), no_repo: repo.nil?)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Bundle.new(lockfile: inside_file(dir, lockfile), gemfile: inside_file(dir, gemfile), lock_label: label.(lockfile),
|
|
184
|
+
gemfile_label: label.(gemfile), dir: File.realpath(dir), trusted: repo, outside: nil, no_repo: false)
|
|
185
|
+
rescue SystemCallError
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
private_class_method :boot_bundle
|
|
189
|
+
|
|
190
|
+
# The BUNDLE_GEMFILE config/boot.rb sets, when it is outside the app root.
|
|
191
|
+
def boot_gemfile(root)
|
|
192
|
+
real_root = File.realpath(root)
|
|
193
|
+
real = File.realpath(File.join(real_root, "config/boot.rb"))
|
|
194
|
+
return nil unless SafePath.contained?(real, real_root)
|
|
195
|
+
|
|
196
|
+
result = ruby_parse(real) or return nil
|
|
197
|
+
relative, anchor = Introspectors::AstWalk.each(result.value).lazy.filter_map { |node| bundle_gemfile_path(node) }.first
|
|
198
|
+
return nil unless relative
|
|
199
|
+
|
|
200
|
+
# Relative to __FILE__ the path starts from boot.rb itself, one level below __dir__.
|
|
201
|
+
target = File.expand_path(relative, anchor == :file ? real : File.dirname(real))
|
|
202
|
+
target unless target.start_with?(SafePath.dir_prefix(real_root))
|
|
203
|
+
rescue SystemCallError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
private_class_method :boot_gemfile
|
|
207
|
+
|
|
208
|
+
# `ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)`, the line `rails new`
|
|
209
|
+
# and `rails plugin new` write, with `=` or `__FILE__` as older templates do.
|
|
210
|
+
def bundle_gemfile_path(node)
|
|
211
|
+
value = case node
|
|
212
|
+
when Prism::IndexOrWriteNode then node.value if env_index?(node.receiver, node.arguments&.arguments)
|
|
213
|
+
when Prism::CallNode then node.arguments&.arguments&.last if node.name == :[]= && env_index?(node.receiver, node.arguments&.arguments&.first(1))
|
|
214
|
+
end
|
|
215
|
+
return nil unless value.is_a?(Prism::CallNode) && value.name == :expand_path && value.receiver.is_a?(Prism::ConstantReadNode) &&
|
|
216
|
+
value.receiver.name == :File
|
|
217
|
+
|
|
218
|
+
path, anchor = value.arguments&.arguments
|
|
219
|
+
return nil unless path.is_a?(Prism::StringNode) && value.arguments.arguments.size == 2
|
|
220
|
+
|
|
221
|
+
if anchor.is_a?(Prism::SourceFileNode) then [ path.unescaped, :file ]
|
|
222
|
+
elsif anchor.is_a?(Prism::CallNode) && anchor.name == :__dir__ && anchor.receiver.nil? then [ path.unescaped, :dir ]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
private_class_method :bundle_gemfile_path
|
|
226
|
+
|
|
227
|
+
def env_index?(receiver, arguments)
|
|
228
|
+
receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :ENV && arguments&.size == 1 && literal(arguments.first) == "BUNDLE_GEMFILE"
|
|
229
|
+
end
|
|
230
|
+
private_class_method :env_index?
|
|
231
|
+
|
|
232
|
+
# The file's real path when it exists and does not link out of its directory.
|
|
233
|
+
def inside_file(dir, name)
|
|
234
|
+
real = File.realpath(File.join(dir, name))
|
|
235
|
+
real if SafePath.contained?(real, File.realpath(dir))
|
|
236
|
+
rescue SystemCallError
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
private_class_method :inside_file
|
|
240
|
+
|
|
241
|
+
def unread_bundle(outside, no_repo)
|
|
242
|
+
where = no_repo ? "and the app is in no git repository" : "outside the app's git repository"
|
|
243
|
+
"config/boot.rb points Bundler at `#{outside}`, #{where}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def absent_spec(root, bundle)
|
|
247
|
+
reason = if bundle.outside
|
|
248
|
+
"No #{lockfile_name(root)} in the app; #{unread_bundle(bundle.outside, bundle.no_repo)}, so that bundle is not read"
|
|
249
|
+
else
|
|
250
|
+
"No #{bundle.lock_label} found"
|
|
251
|
+
end
|
|
252
|
+
facts = gemfile(bundle)
|
|
253
|
+
Spec.new({}, **declared_ruby(nil, root, bundle, facts), reason: reason, absent: true, outside_gemfile: bundle.outside,
|
|
254
|
+
no_repo: bundle.no_repo, gemfile_gems: facts[:gems])
|
|
255
|
+
end
|
|
256
|
+
private_class_method :absent_spec
|
|
257
|
+
|
|
118
258
|
def mtime(path)
|
|
119
259
|
File.mtime(path)
|
|
120
260
|
rescue SystemCallError
|
|
121
261
|
nil
|
|
122
262
|
end
|
|
123
|
-
private_class_method :mtime
|
|
124
263
|
|
|
125
|
-
def parse(
|
|
264
|
+
def parse(bundle, root)
|
|
265
|
+
path = bundle.lockfile
|
|
126
266
|
content = SafeFile.read(path, max_size: MAX_SIZE)
|
|
127
|
-
return Spec.new({}, reason: "
|
|
267
|
+
return Spec.new({}, reason: "#{File.basename(path)} could not be read") unless content
|
|
128
268
|
|
|
129
269
|
versions = {}
|
|
130
270
|
direct = Set.new
|
|
@@ -152,52 +292,140 @@ module RailsAiContext
|
|
|
152
292
|
# prerelease tag ("1.70.0-beta1") is dropped along with the platform.
|
|
153
293
|
versions[match[1]] ||= match[2].split("-", 2).first
|
|
154
294
|
elsif (match = line.match(RUBY_LINE)) && match[1].match?(PLAIN_VERSION)
|
|
155
|
-
ruby_version = match[1]
|
|
295
|
+
ruby_version = [ match[1], engine_name(match[2], match[3]) ]
|
|
156
296
|
end
|
|
157
297
|
end
|
|
158
298
|
# An empty Gemfile still locks to a file with a specs: section, so no
|
|
159
299
|
# gems is an answer there. A file without one is not a lockfile at all,
|
|
160
300
|
# and answering it as an app with no gems denies every gem it holds.
|
|
161
|
-
return Spec.new({}, reason: "
|
|
301
|
+
return Spec.new({}, reason: "#{File.basename(path)} has no specs section") unless specs_section
|
|
162
302
|
|
|
163
|
-
|
|
303
|
+
facts = gemfile(bundle)
|
|
304
|
+
Spec.new(versions, **declared_ruby(ruby_version, root, bundle, facts), direct: direct, path_remotes: path_remotes)
|
|
164
305
|
end
|
|
165
306
|
private_class_method :parse
|
|
166
307
|
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
|
|
308
|
+
# `gems` is nil when a call adds gems the Gemfile does not name (gemspec, a gem
|
|
309
|
+
# or eval_gemfile whose argument is not a literal, a file left unread).
|
|
310
|
+
def gemfile(bundle)
|
|
311
|
+
entries = gemfile_entries(bundle) or return { ruby: nil, gems: nil }
|
|
312
|
+
|
|
313
|
+
ruby = entries.find { |entry| entry[:type] == :ruby }
|
|
314
|
+
gems = entries.filter_map { |entry| entry[:name] if entry[:type] == :gem }.uniq unless entries.any? { |entry| entry[:type] == :unknown_gems }
|
|
315
|
+
{ ruby: ruby && gemfile_ruby(ruby), gems: gems }
|
|
316
|
+
end
|
|
317
|
+
private_class_method :gemfile
|
|
318
|
+
|
|
319
|
+
# Before the gem loads, a plain walk of the one Gemfile; eval_gemfile is left unread.
|
|
320
|
+
def gemfile_entries(bundle)
|
|
321
|
+
return Introspectors::GemfileGems.read_bundle(bundle) if loaded?
|
|
322
|
+
|
|
323
|
+
result = ruby_parse(bundle.gemfile) or return nil
|
|
324
|
+
Introspectors::AstWalk.each(result.value).filter_map do |node|
|
|
325
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
326
|
+
|
|
327
|
+
args = node.arguments&.arguments || []
|
|
328
|
+
case node.name
|
|
329
|
+
when :ruby
|
|
330
|
+
options = args.grep(Prism::KeywordHashNode).flat_map(&:elements).grep(Prism::AssocNode).to_h { |pair| [ literal(pair.key), literal(pair.value) ] }
|
|
331
|
+
{ type: :ruby, version: literal(args.first), engine: options["engine"], engine_version: options["engine_version"] }
|
|
332
|
+
when :gem then args.first.is_a?(Prism::StringNode) ? { type: :gem, name: args.first.unescaped } : { type: :unknown_gems }
|
|
333
|
+
when :gemspec, :eval_gemfile then { type: :unknown_gems }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
private_class_method :gemfile_entries
|
|
338
|
+
|
|
339
|
+
def loaded?
|
|
340
|
+
defined?(Introspectors::GemfileGems) ? true : false
|
|
341
|
+
end
|
|
342
|
+
private_class_method :loaded?
|
|
343
|
+
|
|
344
|
+
# AstCache once the gem is loaded; before the boot Prism alone, as AstCache's
|
|
345
|
+
# concurrent-ruby would load ahead of the app's bundle.
|
|
346
|
+
def ruby_parse(path)
|
|
347
|
+
return nil unless path
|
|
348
|
+
return AstCache.parse(File.realpath(path)) if loaded?
|
|
349
|
+
|
|
350
|
+
require "prism"
|
|
351
|
+
require_relative "introspectors/ast_walk"
|
|
171
352
|
content = SafeFile.read(path, max_size: MAX_SIZE)
|
|
172
|
-
|
|
353
|
+
content && Prism.parse(content)
|
|
354
|
+
rescue SystemCallError, ArgumentError, LoadError
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
private_class_method :ruby_parse
|
|
173
358
|
|
|
174
|
-
|
|
175
|
-
|
|
359
|
+
# A requirement such as `ruby ">= 3.3.0"` names a range, not a version, so it is left unanswered.
|
|
360
|
+
def gemfile_ruby(entry)
|
|
361
|
+
version = entry[:version]
|
|
362
|
+
[ version, engine_name(entry[:engine], entry[:engine_version]) ] if version&.match?(PLAIN_VERSION)
|
|
176
363
|
end
|
|
177
|
-
private_class_method :
|
|
364
|
+
private_class_method :gemfile_ruby
|
|
178
365
|
|
|
179
|
-
def
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
366
|
+
def literal(node)
|
|
367
|
+
node.unescaped if node.is_a?(Prism::StringNode) || node.is_a?(Prism::SymbolNode)
|
|
368
|
+
end
|
|
369
|
+
private_class_method :literal
|
|
370
|
+
|
|
371
|
+
# Sources in Bundler's order; an engine's Ruby version comes only from its own source (JRuby 9.4 runs Ruby 3.1).
|
|
372
|
+
def declared_ruby(locked, root, bundle, facts)
|
|
373
|
+
declared = {
|
|
374
|
+
bundle.lock_label => locked,
|
|
375
|
+
bundle.gemfile_label => facts[:ruby],
|
|
376
|
+
".ruby-version" => version_string(read_inside(root, ".ruby-version")&.strip),
|
|
377
|
+
".tool-versions" => version_string(read_inside(root, ".tool-versions")&.[](TOOL_VERSIONS_RUBY, 1)),
|
|
378
|
+
**mise_ruby(root)
|
|
185
379
|
}.compact
|
|
380
|
+
engine = declared.values.first&.last
|
|
381
|
+
declared = declared.first(1).to_h if engine
|
|
382
|
+
{ ruby_versions: declared.transform_values(&:first).compact, ruby_engine: engine }
|
|
186
383
|
end
|
|
187
|
-
private_class_method :
|
|
384
|
+
private_class_method :declared_ruby
|
|
188
385
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
386
|
+
# SafePath's containment without its sensitive-pattern check, which needs
|
|
387
|
+
# the configuration: the CLI reads GemLock before loading it.
|
|
388
|
+
def read_inside(root, relative)
|
|
389
|
+
real = File.realpath(File.join(root, relative))
|
|
390
|
+
SafeFile.read(real, max_size: MAX_SIZE) if SafePath.contained?(real, File.realpath(root))
|
|
391
|
+
rescue SystemCallError
|
|
392
|
+
nil
|
|
193
393
|
end
|
|
194
|
-
private_class_method :
|
|
394
|
+
private_class_method :read_inside
|
|
195
395
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
396
|
+
# The ruby tool of the mise file that wins, keyed by that file's name.
|
|
397
|
+
def mise_ruby(root)
|
|
398
|
+
MISE_FILES.each do |name|
|
|
399
|
+
content = read_inside(root, name)
|
|
400
|
+
tools = content&.[](MISE_TOOLS, 1)
|
|
401
|
+
declared = version_string(tools&.[](MISE_RUBY, 1))
|
|
402
|
+
return { name => declared } if declared
|
|
403
|
+
end
|
|
404
|
+
{}
|
|
405
|
+
end
|
|
406
|
+
private_class_method :mise_ruby
|
|
407
|
+
|
|
408
|
+
# "3.4.9" and "ruby-3.4.9" are CRuby; "jruby-9.4.8.0" names an engine
|
|
409
|
+
# version, not the Ruby version that engine implements, and a bare "jruby" its latest.
|
|
410
|
+
def version_string(declared)
|
|
411
|
+
return nil if declared.nil?
|
|
412
|
+
|
|
413
|
+
if (match = declared.match(ENGINE_PREFIXED)) && match[1] != "ruby"
|
|
414
|
+
[ nil, engine_name(match[1], match[2]) ]
|
|
415
|
+
elsif ENGINE_NAMES.key?(declared)
|
|
416
|
+
[ nil, engine_name(declared, nil) ]
|
|
417
|
+
else
|
|
418
|
+
version = declared.sub(/\Aruby-/, "")
|
|
419
|
+
[ version, nil ] if version.match?(PLAIN_VERSION)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
private_class_method :version_string
|
|
423
|
+
|
|
424
|
+
def engine_name(engine, version)
|
|
425
|
+
return nil if engine.nil? || engine == "ruby"
|
|
426
|
+
|
|
427
|
+
[ ENGINE_NAMES.fetch(engine, engine), version ].compact.join(" ")
|
|
200
428
|
end
|
|
201
|
-
private_class_method :
|
|
429
|
+
private_class_method :engine_name
|
|
202
430
|
end
|
|
203
431
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "open3"
|
|
4
|
-
|
|
5
3
|
module RailsAiContext
|
|
6
4
|
# git's ignore-file rules (globs, anchoring, last match wins, negation) for
|
|
7
5
|
# the doctor, and ripgrep's walk over a tree for the search fallback.
|
|
@@ -147,7 +145,7 @@ module RailsAiContext
|
|
|
147
145
|
def global
|
|
148
146
|
return @global if defined?(@global)
|
|
149
147
|
|
|
150
|
-
@global = source(@root, GitIgnore.global_excludes_path
|
|
148
|
+
@global = source(@root, GitIgnore.global_excludes_path)
|
|
151
149
|
end
|
|
152
150
|
end
|
|
153
151
|
|
|
@@ -169,15 +167,19 @@ module RailsAiContext
|
|
|
169
167
|
nil
|
|
170
168
|
end
|
|
171
169
|
|
|
172
|
-
# `core.excludesFile` as
|
|
173
|
-
#
|
|
174
|
-
def global_excludes_path
|
|
175
|
-
configured, status = Open3.capture2("git", "config", "--get", "core.excludesFile", chdir: root)
|
|
176
|
-
configured = configured.to_s.strip
|
|
177
|
-
return File.expand_path(configured) if status.success? && !configured.empty?
|
|
178
|
-
|
|
170
|
+
# `core.excludesFile` as ripgrep 15 finds it: ~/.gitconfig, then the XDG
|
|
171
|
+
# git config, then git's default (ignore crate `gitconfig_excludes_path`).
|
|
172
|
+
def global_excludes_path
|
|
179
173
|
xdg = ENV["XDG_CONFIG_HOME"].to_s
|
|
180
|
-
|
|
174
|
+
config_dir = xdg.empty? ? File.join(Dir.home, ".config") : xdg
|
|
175
|
+
# A setting that is not UTF-8 is skipped, as ripgrep skips it.
|
|
176
|
+
configured = [ File.join(Dir.home, ".gitconfig"), File.join(config_dir, "git", "config") ].lazy.filter_map do |config|
|
|
177
|
+
path = File.file?(config) && File.binread(config)[/^\s*excludesfile\s*=\s*"?\s*(\S+?)\s*"?\s*$/i, 1]
|
|
178
|
+
path && path.force_encoding(Encoding::UTF_8).valid_encoding? ? path : nil
|
|
179
|
+
end.first
|
|
180
|
+
return File.expand_path(configured.gsub("~", Dir.home)) if configured
|
|
181
|
+
|
|
182
|
+
File.join(config_dir, "git", "ignore")
|
|
181
183
|
rescue StandardError
|
|
182
184
|
nil
|
|
183
185
|
end
|
|
@@ -26,14 +26,20 @@ module RailsAiContext
|
|
|
26
26
|
def self.format_hint(hint)
|
|
27
27
|
lines = []
|
|
28
28
|
lines << "### #{hint.model_name} #{hint.confidence}"
|
|
29
|
-
lines <<
|
|
29
|
+
lines << if hint.embedded_in
|
|
30
|
+
"**Embedded in:** `#{hint.embedded_in}` (no collection of its own)"
|
|
31
|
+
elsif hint.collection
|
|
32
|
+
"**Collection:** `#{hint.table_name}` (key: `#{hint.primary_key}`)"
|
|
33
|
+
else
|
|
34
|
+
"**Table:** `#{hint.table_name}` (pk: `#{hint.primary_key}`)"
|
|
35
|
+
end
|
|
30
36
|
|
|
31
37
|
if hint.columns.any?
|
|
32
38
|
col_summary = hint.columns.first(10).map { |c|
|
|
33
39
|
"`#{c[:name]}` #{c[:type]}#{'[]' if c[:array]}#{c[:null] == false ? ' NOT NULL' : ''}"
|
|
34
40
|
}
|
|
35
41
|
col_summary << "... #{hint.columns.size - 10} more" if hint.columns.size > 10
|
|
36
|
-
lines << "
|
|
42
|
+
lines << "**#{hint.collection ? 'Fields' : 'Columns'}:** #{col_summary.join(', ')}"
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
if hint.associations.any?
|
|
@@ -19,10 +19,13 @@ module RailsAiContext
|
|
|
19
19
|
model_info = models_data[model_key]
|
|
20
20
|
return nil unless model_info.is_a?(Hash)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
mongoid = model_info[:mongoid] == true
|
|
23
|
+
table_name = mongoid ? model_info[:collection] : model_info[:table_name]
|
|
24
|
+
table_data = Payload.model_table(schema_data, model_info) if table_name && !mongoid
|
|
24
25
|
|
|
25
|
-
columns = if
|
|
26
|
+
columns = if mongoid
|
|
27
|
+
Array(model_info[:fields]).map { |field| { name: field[:name].to_s, type: field[:type] }.compact }
|
|
28
|
+
elsif table_data
|
|
26
29
|
(table_data[:columns] || []).map do |col|
|
|
27
30
|
{ name: col[:name], type: col[:type], null: col[:null], array: col[:array] }.compact
|
|
28
31
|
end
|
|
@@ -47,7 +50,7 @@ module RailsAiContext
|
|
|
47
50
|
}.compact
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
primary_key = table_data&.dig(:primary_key) || "id"
|
|
53
|
+
primary_key = mongoid ? "_id" : table_data&.dig(:primary_key) || "id"
|
|
51
54
|
|
|
52
55
|
# A static record already carries [STATIC]; a booted one has no
|
|
53
56
|
# confidence key, and a resolved table there is reflection-confirmed.
|
|
@@ -60,7 +63,9 @@ module RailsAiContext
|
|
|
60
63
|
associations: associations,
|
|
61
64
|
validations: validations,
|
|
62
65
|
primary_key: Introspectors::SchemaConventions.primary_key_label(primary_key),
|
|
63
|
-
confidence: confidence
|
|
66
|
+
confidence: confidence,
|
|
67
|
+
collection: mongoid,
|
|
68
|
+
embedded_in: (model_info[:embedded_in] if mongoid)
|
|
64
69
|
)
|
|
65
70
|
end
|
|
66
71
|
end
|
|
@@ -24,6 +24,7 @@ module RailsAiContext
|
|
|
24
24
|
context = {
|
|
25
25
|
app_name: app_name,
|
|
26
26
|
ruby_version: ruby_version,
|
|
27
|
+
ruby_engine: ruby_engine,
|
|
27
28
|
rails_version: rails_version,
|
|
28
29
|
environment: environment_name,
|
|
29
30
|
tier: RailsAiContext.static_tier? ? "static" : "booted",
|
|
@@ -203,6 +204,14 @@ module RailsAiContext
|
|
|
203
204
|
GemLock.for(app.root).ruby_version || Confidence.unavailable("app declares none")
|
|
204
205
|
end
|
|
205
206
|
|
|
207
|
+
# Nil for CRuby, else the engine and its own version ("JRuby 9.4.8.0").
|
|
208
|
+
def ruby_engine
|
|
209
|
+
return GemLock.for(app.root).ruby_engine if RailsAiContext.static_tier?
|
|
210
|
+
return nil if RUBY_ENGINE == "ruby"
|
|
211
|
+
|
|
212
|
+
[ GemLock::ENGINE_NAMES.fetch(RUBY_ENGINE, RUBY_ENGINE), RUBY_ENGINE_VERSION ].join(" ")
|
|
213
|
+
end
|
|
214
|
+
|
|
206
215
|
def rails_version
|
|
207
216
|
return Rails.version if defined?(Rails) && Rails.respond_to?(:version) && !RailsAiContext.static_tier?
|
|
208
217
|
|
|
@@ -210,7 +219,7 @@ module RailsAiContext
|
|
|
210
219
|
# mid-sentence into files the user commits. An app can bundle railties
|
|
211
220
|
# without the `rails` meta-gem and still be a Rails app at that version.
|
|
212
221
|
lock = GemLock.for(app.root)
|
|
213
|
-
lock.version("rails") || lock.version("railties") || Confidence.unavailable("app not booted")
|
|
222
|
+
lock.version("rails") || lock.version("railties") || Confidence.unavailable(lock.outside_gemfile ? lock.reason : "app not booted")
|
|
214
223
|
end
|
|
215
224
|
|
|
216
225
|
def environment_name
|
|
@@ -2,45 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
module Introspectors
|
|
5
|
-
# Discovers Action Mailbox setup: mailbox classes
|
|
5
|
+
# Discovers Action Mailbox setup: mailbox classes and the routing that sends mail to them.
|
|
6
6
|
class ActionMailboxIntrospector < Base
|
|
7
7
|
extend StaticTier
|
|
8
8
|
static_tier :files_only
|
|
9
9
|
|
|
10
|
+
BASE_FILE = "application_mailbox.rb"
|
|
11
|
+
|
|
10
12
|
def call
|
|
13
|
+
mailboxes, routes = extract
|
|
11
14
|
{
|
|
12
15
|
installed: defined?(ActionMailbox) ? true : false,
|
|
13
|
-
mailboxes:
|
|
16
|
+
mailboxes: mailboxes,
|
|
17
|
+
routes: routes
|
|
14
18
|
}
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
private
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
# Rails keeps one router for every mailbox, filled in load order, and the base loads first.
|
|
24
|
+
def extract
|
|
20
25
|
dir = File.join(root, "app/mailboxes")
|
|
21
|
-
return [] unless Dir.exist?(dir)
|
|
22
|
-
|
|
23
|
-
Dir.glob(File.join(dir, "**/*.rb")).filter_map do |path|
|
|
24
|
-
relative = path.sub("#{dir}/", "")
|
|
25
|
-
next if relative == "application_mailbox.rb"
|
|
26
|
+
return [ [], [] ] unless Dir.exist?(dir)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
paths = Dir.glob(File.join(dir, "**/*.rb")).sort_by { |path| [ path == File.join(dir, BASE_FILE) ? 0 : 1, path ] }
|
|
29
|
+
mailboxes = []
|
|
30
|
+
routes = []
|
|
31
|
+
paths.each do |path|
|
|
32
|
+
relative = path.delete_prefix("#{root}/")
|
|
33
|
+
results = SourceIntrospector.walk(path, { mailbox: Listeners::MailboxRoutingListener })[:mailbox]
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
{ pattern: r[:pattern],
|
|
35
|
+
results.select { |r| r[:type] == :routing }.each do |r|
|
|
36
|
+
routes << { pattern: r[:pattern], mailbox: "#{r[:action].camelize}Mailbox", file: relative }
|
|
32
37
|
end
|
|
38
|
+
next if path == File.join(dir, BASE_FILE)
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
entry = { name: name, file: relative, routing: routing }
|
|
40
|
+
entry = { name: path.delete_prefix("#{dir}/").delete_suffix(".rb").camelize, file: relative }
|
|
41
|
+
callbacks = results.select { |r| r[:type] == :callback }.map { |r| { type: r[:callback_type], method: r[:method] } }
|
|
39
42
|
entry[:callbacks] = callbacks if callbacks.any?
|
|
40
|
-
entry
|
|
43
|
+
mailboxes << entry
|
|
41
44
|
rescue => e
|
|
42
45
|
RailsAiContext.debug_fail(e, nil, label: "extract_mailboxes")
|
|
43
|
-
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
mailboxes.each do |m|
|
|
49
|
+
m[:routed_from] = routes.select { |r| r[:mailbox] == m[:name] }.map { |r| r[:pattern] }
|
|
50
|
+
end
|
|
51
|
+
[ mailboxes.sort_by { |m| m[:name] }.map { |m| m.slice(:name, :file, :routed_from, :callbacks) }, routes ]
|
|
44
52
|
end
|
|
45
53
|
end
|
|
46
54
|
end
|