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
|
@@ -23,25 +23,30 @@ module RailsAiContext
|
|
|
23
23
|
# a dump that parses to zero tables falls through to the next source.
|
|
24
24
|
def self.for(root)
|
|
25
25
|
root = root.to_s
|
|
26
|
+
# One parse per run however many introspectors ask.
|
|
27
|
+
RailsAiContext::RunCache.fetch([ :schema_reader, root ]) { choose(root) }
|
|
28
|
+
end
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
def self.choose(root)
|
|
31
|
+
return from_tables({}, source: :none, path: nil) if RailsAiContext::AppKind.sequel_schema(root)
|
|
32
|
+
|
|
33
|
+
candidates = SchemaDumpPath.candidates(root)
|
|
34
|
+
candidates.each do |format, path|
|
|
35
|
+
next unless File.exist?(path)
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if format == :ruby
|
|
38
|
+
reader = new(path, pk_type: SchemaConventions.implicit_pk_type(root))
|
|
39
|
+
return reader.with_source(:schema_rb) if reader.tables.any?
|
|
40
|
+
else
|
|
41
|
+
content = RailsAiContext::SafeFile.read(path, max_size: RailsAiContext.configuration.max_schema_file_size)
|
|
42
|
+
parsed = content && StructureSqlReader.parse(content)
|
|
43
|
+
return from_tables(parsed[:tables], source: :structure_sql, path: path, views: parsed[:views]) if parsed && parsed[:tables].any?
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
migrate_dirs = MigrationReplay.migration_dirs(root)
|
|
43
|
-
if MigrationReplay.migration_files(migrate_dirs).any?
|
|
44
|
-
pk_type = SchemaConventions.implicit_pk_type(root
|
|
48
|
+
if MigrationReplay.migration_files(migrate_dirs, root: root).any?
|
|
49
|
+
pk_type = SchemaConventions.implicit_pk_type(root)
|
|
45
50
|
return from_tables(MigrationReplay.tables(migrate_dirs, pk_type: pk_type, root: root),
|
|
46
51
|
source: :migrations, path: migrate_dirs.first)
|
|
47
52
|
end
|
|
@@ -51,9 +56,9 @@ module RailsAiContext
|
|
|
51
56
|
|
|
52
57
|
# A reader over an already-parsed tables hash, for the sources that do
|
|
53
58
|
# not go through the schema.rb event fold.
|
|
54
|
-
def self.from_tables(tables, source:, path:)
|
|
59
|
+
def self.from_tables(tables, source:, path:, views: {})
|
|
55
60
|
reader = allocate
|
|
56
|
-
reader.send(:initialize_from_tables, tables, source, path)
|
|
61
|
+
reader.send(:initialize_from_tables, tables, source, path, views)
|
|
57
62
|
reader
|
|
58
63
|
end
|
|
59
64
|
|
|
@@ -89,11 +94,31 @@ module RailsAiContext
|
|
|
89
94
|
parse[:enums]
|
|
90
95
|
end
|
|
91
96
|
|
|
92
|
-
# @return [Array<Hash>] { table:, expression: } per declared constraint
|
|
97
|
+
# @return [Array<Hash>] { table:, name:, expression: } per declared constraint, name when given
|
|
93
98
|
def check_constraints
|
|
94
99
|
parse[:check_constraints]
|
|
95
100
|
end
|
|
96
101
|
|
|
102
|
+
# @return [Array<String>] the extensions the dump enables
|
|
103
|
+
def extensions
|
|
104
|
+
parse[:extensions]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Hash] view name => { materialized:, sql: }
|
|
108
|
+
def views
|
|
109
|
+
parse[:views]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Hash] virtual table name => { module:, arguments: }
|
|
113
|
+
def virtual_tables
|
|
114
|
+
parse[:virtual_tables]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Hash] table name => why the dumper wrote a comment in its place
|
|
118
|
+
def not_dumped
|
|
119
|
+
parse[:not_dumped]
|
|
120
|
+
end
|
|
121
|
+
|
|
97
122
|
# Declared defaults for one table, as source text. Callers report these
|
|
98
123
|
# verbatim when the live database returns no default.
|
|
99
124
|
def defaults_for(table)
|
|
@@ -114,7 +139,7 @@ module RailsAiContext
|
|
|
114
139
|
|
|
115
140
|
attr_reader :path
|
|
116
141
|
|
|
117
|
-
def initialize_from_tables(tables, source, path)
|
|
142
|
+
def initialize_from_tables(tables, source, path, views)
|
|
118
143
|
@path = path
|
|
119
144
|
@pk_type = nil
|
|
120
145
|
@source = source
|
|
@@ -123,10 +148,15 @@ module RailsAiContext
|
|
|
123
148
|
# structure.sql and the replay keep from_table/to_table on the table;
|
|
124
149
|
# every reader answers the schema.rb shape, so no consumer checks both.
|
|
125
150
|
foreign_keys: tables.flat_map { |_name, t| t[:foreign_keys] || [] }.map do |fk|
|
|
126
|
-
{ from: fk[:from_table], to: fk[:to_table], column: fk[:column], primary_key: fk[:primary_key]
|
|
151
|
+
{ from: fk[:from_table], to: fk[:to_table], column: fk[:column], primary_key: fk[:primary_key],
|
|
152
|
+
on_delete: fk[:on_delete], on_update: fk[:on_update] }.compact
|
|
127
153
|
end,
|
|
128
154
|
enums: [],
|
|
129
|
-
check_constraints: []
|
|
155
|
+
check_constraints: [],
|
|
156
|
+
extensions: [],
|
|
157
|
+
views: views,
|
|
158
|
+
virtual_tables: {},
|
|
159
|
+
not_dumped: {}
|
|
130
160
|
}
|
|
131
161
|
end
|
|
132
162
|
|
|
@@ -139,7 +169,7 @@ module RailsAiContext
|
|
|
139
169
|
end
|
|
140
170
|
|
|
141
171
|
def empty_schema
|
|
142
|
-
{ tables: {}, foreign_keys: [], enums: [], check_constraints: [] }
|
|
172
|
+
{ tables: {}, foreign_keys: [], enums: [], check_constraints: [], extensions: [], views: {}, virtual_tables: {}, not_dumped: {} }
|
|
143
173
|
end
|
|
144
174
|
|
|
145
175
|
def build
|
|
@@ -213,18 +243,27 @@ module RailsAiContext
|
|
|
213
243
|
when :unread_call
|
|
214
244
|
SchemaConventions.note_unread_call(schema[:tables][current], event[:name]) if current
|
|
215
245
|
when :add_index
|
|
216
|
-
schema[:tables][event[:table]]
|
|
246
|
+
target = schema[:tables][event[:table]] || schema[:views][event[:table]]
|
|
247
|
+
(target[:indexes] ||= []) << index_entry(event) if target
|
|
217
248
|
when :foreign_key
|
|
218
|
-
schema[:foreign_keys] <<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
schema[:foreign_keys] << event.slice(:from, :to, :column, :primary_key, *SchemaConventions::FOREIGN_KEY_OPTIONS)
|
|
250
|
+
when :unique_constraint
|
|
251
|
+
table = schema[:tables][current] if current
|
|
252
|
+
(table[:unique_constraints] ||= []) << event.slice(:columns, :options) if table
|
|
253
|
+
when :extension
|
|
254
|
+
schema[:extensions] << event[:name]
|
|
222
255
|
when :enum
|
|
223
256
|
schema[:enums] << { name: event[:name], values: event[:values] }
|
|
224
257
|
when :check_constraint
|
|
225
|
-
schema[:check_constraints] << { table: current,
|
|
258
|
+
schema[:check_constraints] << { table: current, **event.slice(:name, :expression) } if current
|
|
226
259
|
when :add_check_constraint
|
|
227
|
-
schema[:check_constraints] <<
|
|
260
|
+
schema[:check_constraints] << event.slice(:table, :name, :expression)
|
|
261
|
+
when :view
|
|
262
|
+
schema[:views][event[:name]] = { materialized: event[:materialized], sql: event[:sql] }
|
|
263
|
+
when :virtual_table
|
|
264
|
+
schema[:virtual_tables][event[:name]] = event.slice(:module, :arguments)
|
|
265
|
+
when :not_dumped
|
|
266
|
+
schema[:not_dumped][event[:table]] = event[:reason]
|
|
228
267
|
end
|
|
229
268
|
|
|
230
269
|
current
|
|
@@ -236,8 +275,9 @@ module RailsAiContext
|
|
|
236
275
|
name: column_name(event),
|
|
237
276
|
type: event[:column_type],
|
|
238
277
|
default: default_for(event, options),
|
|
239
|
-
options: options
|
|
240
|
-
|
|
278
|
+
options: options,
|
|
279
|
+
virtual: event[:virtual]
|
|
280
|
+
}.compact
|
|
241
281
|
end
|
|
242
282
|
|
|
243
283
|
def index_entry(event)
|
|
@@ -2,28 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
module Introspectors
|
|
5
|
-
# Which classes under
|
|
5
|
+
# Which classes under the service roots are services and which are only a base. The tool's
|
|
6
6
|
# listing and the generated Services line both ask here, so they name one set.
|
|
7
7
|
module ServiceClasses
|
|
8
|
+
# active_interaction recommends app/interactions, and interactor-rails
|
|
9
|
+
# autoloads and generates into app/interactors.
|
|
10
|
+
ROOTS = %w[app/services app/interactions app/interactors].freeze
|
|
11
|
+
|
|
8
12
|
module_function
|
|
9
13
|
|
|
10
14
|
# The declared constant and its superclass from one parse; the path fills in a
|
|
11
15
|
# namespace the source leaves out.
|
|
12
16
|
#
|
|
13
|
-
# @return [Array(String, String)] name, superclass
|
|
17
|
+
# @return [Array(String, String, Array<String>)] name, superclass, the nesting it is read in
|
|
14
18
|
def declaration(source, path_name)
|
|
15
|
-
own = DeclaredConstant.declaration_for(DeclaredConstant.declarations(source), path_name)
|
|
16
|
-
[ own&.name || DeclaredConstant.resolve(source, path_name), own&.superclass ]
|
|
19
|
+
own = DeclaredConstant.declaration_for(DeclaredConstant.declarations(source, path_name: path_name), path_name)
|
|
20
|
+
[ own&.name || DeclaredConstant.resolve(source, path_name), own&.superclass, own&.nesting ]
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
# The bases among the given [name, superclass]
|
|
23
|
+
# The bases among the given [name, superclass, nesting] triples: a Base-named class
|
|
20
24
|
# something else in the list inherits from, or one of Rails' own.
|
|
21
|
-
def abstract_names(
|
|
22
|
-
named =
|
|
25
|
+
def abstract_names(declared)
|
|
26
|
+
named = declared.map(&:first).select { |name| SuperclassChain.abstract_base_name?(name) }
|
|
23
27
|
return [] if named.empty?
|
|
24
28
|
|
|
25
|
-
inherited =
|
|
26
|
-
SuperclassChain.resolve_in_scope(name, superclass) { |candidate| candidate if named.include?(candidate) }
|
|
29
|
+
inherited = declared.filter_map do |name, superclass, nesting|
|
|
30
|
+
SuperclassChain.resolve_in_scope(name, superclass, nesting: nesting) { |candidate| candidate if named.include?(candidate) }
|
|
27
31
|
end
|
|
28
32
|
named.select { |name| SuperclassChain.abstract_base?(name, inherited: inherited.include?(name)) }.uniq.sort
|
|
29
33
|
end
|
|
@@ -82,14 +86,16 @@ module RailsAiContext
|
|
|
82
86
|
pairs = []
|
|
83
87
|
modules = []
|
|
84
88
|
lookup = SuperclassChain.lookup_for(root)
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
ROOTS.each do |kind|
|
|
90
|
+
SourceScan.each(root, kind: kind) do |record|
|
|
91
|
+
next if concern?(record.file, record.source)
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
name, superclass, nesting = declaration(record.source, record.path_name)
|
|
94
|
+
next if mailer?(record.source, name, superclass, lookup)
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
pairs << [ name, superclass, nesting ]
|
|
97
|
+
modules << name if entryless_module?(record.source, name)
|
|
98
|
+
end
|
|
93
99
|
end
|
|
94
100
|
left_out = abstract_names(pairs) + mixed_in(root, modules)
|
|
95
101
|
pairs.map(&:first).reject { |name| left_out.include?(name) }.uniq.sort
|
|
@@ -19,7 +19,7 @@ module RailsAiContext
|
|
|
19
19
|
# framework, the standard library, and the app's record base.
|
|
20
20
|
IGNORED = %w[
|
|
21
21
|
Rails ActiveRecord ApplicationRecord File Dir ENV String Integer Float
|
|
22
|
-
Array Hash Set Time Date DateTime URI Regexp
|
|
22
|
+
Array Hash Set Time Date DateTime URI Regexp Class
|
|
23
23
|
].freeze
|
|
24
24
|
|
|
25
25
|
# `::Foo::Bar` and `Foo::Bar` are one constant; the root operator is not part of the name.
|
|
@@ -49,8 +49,9 @@ module RailsAiContext
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Walk a file with a custom listener map. Returns { key => results_array }.
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
# A caller that already read the file passes `source`, so its other reads share the parse.
|
|
53
|
+
def self.walk(path, listener_map = LISTENER_MAP, source: nil)
|
|
54
|
+
result = source ? AstCache.parse_string(source) : AstCache.parse(path)
|
|
54
55
|
walk_dispatch(result, listener_map)
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -60,6 +61,20 @@ module RailsAiContext
|
|
|
60
61
|
walk_dispatch(result, listener_map)
|
|
61
62
|
end
|
|
62
63
|
|
|
64
|
+
# The calls a class body makes itself, out of what one walk found: a call
|
|
65
|
+
# inside a `def` runs only when the method is called.
|
|
66
|
+
def self.outside_defs(calls, methods)
|
|
67
|
+
bodies = methods.respond_to?(:def_spans) ? methods.def_spans : Array(methods).filter_map { |m| m[:offset]...m[:end_offset] if m[:offset] && m[:end_offset] }
|
|
68
|
+
Array(calls).reject { |call| call[:offset] && bodies.any? { |range| range.cover?(call[:offset]) } }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The calls a class body makes itself, out of a walk that ran MethodsListener as `methods:` and
|
|
72
|
+
# NestedConstantsListener as `nested:`: a class or module it nests declares only for itself.
|
|
73
|
+
def self.class_level(calls, walked)
|
|
74
|
+
nested = Array(walked[:nested])
|
|
75
|
+
outside_defs(calls, walked[:methods]).reject { |call| call[:offset] && nested.any? { |range| range.cover?(call[:offset]) } }
|
|
76
|
+
end
|
|
77
|
+
|
|
63
78
|
# Walk a parse result the caller already holds, so a second reader of the
|
|
64
79
|
# same source does not parse it again.
|
|
65
80
|
def self.walk_dispatch(parse_result, listener_map)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module RailsAiContext
|
|
4
6
|
module Introspectors
|
|
5
7
|
# One walk over a kind of app source: every directory PathResolver
|
|
@@ -25,46 +27,190 @@ module RailsAiContext
|
|
|
25
27
|
end.each(&block)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def scan(root, kind, skip_concerns)
|
|
30
|
+
def scan(root, kind, skip_concerns, &block)
|
|
29
31
|
root = root.to_s
|
|
30
32
|
real_root = File.realpath(root)
|
|
31
33
|
PathResolver.dirs_for(root, kind).each do |dir|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
scan_dir(dir, root, real_root, skip_concerns, &block)
|
|
35
|
+
end
|
|
36
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
def scan_dir(dir, root, real_root, skip_concerns)
|
|
41
|
+
real_dir = File.realpath(dir)
|
|
42
|
+
ruby_files(dir, real_dir, [ real_dir, real_root ], Set.new).sort_by(&:first).each do |path, real|
|
|
43
|
+
relative_to_dir = path.delete_prefix(dir + File::SEPARATOR)
|
|
44
|
+
next if skip_concerns && relative_to_dir.start_with?("concerns/")
|
|
45
|
+
next unless within?(real, real_dir, real_root)
|
|
46
|
+
|
|
47
|
+
path_name = relative_to_dir.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
48
|
+
yield Record.new(path: real, file: relative_file(path, real, root, real_root), path_name: path_name, source: nil)
|
|
49
|
+
end
|
|
50
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Zeitwerk follows symlinks, so the walk does too, as far as the app's
|
|
55
|
+
# own tree; a directory reached twice (a link back up) is walked once.
|
|
56
|
+
# Linked directories wait until the real tree is done, so a directory
|
|
57
|
+
# both spell is named by its real path whatever order the disk lists.
|
|
58
|
+
# Each entry is [path as spelled, real path].
|
|
59
|
+
def ruby_files(dir, real_dir, bounds, visited)
|
|
60
|
+
pending = [ [ dir, real_dir ] ]
|
|
61
|
+
found = []
|
|
62
|
+
found.concat(walk_dir(*pending.shift, bounds, visited, pending)) while pending.any?
|
|
63
|
+
found
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# One lstat per entry: below a real directory only a link needs a realpath.
|
|
67
|
+
def walk_dir(dir, real_dir, bounds, visited, links)
|
|
68
|
+
return [] unless visited.add?(real_dir)
|
|
69
|
+
|
|
70
|
+
Dir.children(dir).sort.flat_map do |name|
|
|
71
|
+
next [] if name.start_with?(".")
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
path = File.join(dir, name)
|
|
74
|
+
stat = File.lstat(path)
|
|
75
|
+
if stat.symlink?
|
|
76
|
+
real = File.realpath(path)
|
|
77
|
+
if !File.directory?(real)
|
|
78
|
+
name.end_with?(".rb") ? [ [ path, real ] ] : []
|
|
79
|
+
else
|
|
80
|
+
links << [ path, real ] if within?(real, *bounds)
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
elsif stat.directory?
|
|
84
|
+
walk_dir(path, File.join(real_dir, name), bounds, visited, links)
|
|
85
|
+
else
|
|
86
|
+
name.end_with?(".rb") ? [ [ path, File.join(real_dir, name) ] ] : []
|
|
44
87
|
end
|
|
45
88
|
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
46
|
-
|
|
89
|
+
[]
|
|
47
90
|
end
|
|
48
91
|
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
49
|
-
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def within?(real, real_dir, real_root)
|
|
96
|
+
SafePath.contained?(real, real_dir) || SafePath.contained?(real, real_root)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The model directories plus the classes elsewhere that could be a model.
|
|
100
|
+
# Only the model listing wants the second half: a count or a per-model
|
|
101
|
+
# read of app/models would take in every service with a superclass.
|
|
102
|
+
# A booted caller passes `base_model`, called with the names a superclass the scan does not
|
|
103
|
+
# know resolves to, innermost scope first, since a gem or initializer can define it.
|
|
104
|
+
def model_paths(root, base_model: nil, &block)
|
|
105
|
+
return enum_for(:model_paths, root, base_model: base_model) unless block
|
|
106
|
+
|
|
107
|
+
RunCache.fetch([ :source_scan_models, root.to_s, !base_model.nil? ]) do
|
|
108
|
+
found = paths(root, kind: "app/models", skip_concerns: false).to_a
|
|
109
|
+
found + extra_model_candidates(root.to_s, found, base_model)
|
|
110
|
+
end.each(&block)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
CLASS_WITH_SUPERCLASS = /class[^\S\n]+([\w:]+)[^\S\n]*<[^\S\n]*([\w:]+)/
|
|
114
|
+
MODEL_BASES = %w[ActiveRecord::Base ApplicationRecord].freeze
|
|
115
|
+
|
|
116
|
+
# A model can live in any autoload root; a class there is kept when its superclass is a model base,
|
|
117
|
+
# a model or a class already kept, so a thousand services are not parsed.
|
|
118
|
+
def extra_model_candidates(root, model_records, base_model = nil)
|
|
119
|
+
pending, declared = RunCache.fetch([ :source_scan_model_declarations, root ]) { extra_model_declarations(root, File.realpath(root)) }
|
|
120
|
+
known = model_records.to_set(&:path_name).merge(MODEL_BASES)
|
|
121
|
+
declared = declared | known
|
|
122
|
+
loaded = Hash.new { |cache, candidates| cache[candidates] = base_model ? base_model.call(candidates) : false }
|
|
123
|
+
kept = Set.new
|
|
124
|
+
loop do
|
|
125
|
+
added = pending.select do |record, pairs|
|
|
126
|
+
!kept.include?(record) && pairs.any? do |name, base|
|
|
127
|
+
# Ruby takes the innermost lexical scope that declares the name;
|
|
128
|
+
# a compact `class Admin::Item < Base` has none, a nested one has the path's.
|
|
129
|
+
candidates = lookup(record.path_name, name, base).reverse
|
|
130
|
+
resolved = candidates.find { |candidate| declared.include?(candidate) }
|
|
131
|
+
resolved ? known.include?(resolved) : loaded[candidates]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
break if added.empty?
|
|
135
|
+
|
|
136
|
+
added.each do |record, pairs|
|
|
137
|
+
kept << record
|
|
138
|
+
known.merge(pairs.map { |name, _| qualified(record.path_name, name) })
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
pending.filter_map { |record, _| record if kept.include?(record) }
|
|
142
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
143
|
+
[]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The names `base` can mean where `name` is declared in the file `path_name` names, outermost first.
|
|
147
|
+
def lookup(path_name, name, base = name)
|
|
148
|
+
return [ base.delete_prefix("::") ] if base.start_with?("::")
|
|
149
|
+
|
|
150
|
+
scopes = path_name.split("::")[0...-name.split("::").size]
|
|
151
|
+
(0..scopes.size).map { |depth| [ *scopes.first(depth), base ].join("::") }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The constant a `class name` line in the file `path_name` names declares.
|
|
155
|
+
def qualified(path_name, name)
|
|
156
|
+
return name.delete_prefix("::") if name.start_with?("::")
|
|
157
|
+
|
|
158
|
+
name.include?("::") ? name : lookup(path_name, name).last
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The candidates, and every name the scanned files declare by path or by a superclassed class.
|
|
162
|
+
def extra_model_declarations(root, real_root)
|
|
163
|
+
seen = Set.new
|
|
164
|
+
declared = Set.new
|
|
165
|
+
ignored = PathResolver.ignored_dirs(root).map { |dir| PathResolver.root_key(dir) }
|
|
166
|
+
found = PathResolver.extra_model_roots(root).each_with_object([]) do |dir, records|
|
|
167
|
+
scan_dir(dir, root, real_root, true) do |record|
|
|
168
|
+
next unless seen.add?(record.path)
|
|
169
|
+
next if ignored.any? { |ignored_dir| SafePath.contained?(record.path, ignored_dir) }
|
|
170
|
+
|
|
171
|
+
declared << record.path_name
|
|
172
|
+
pairs = class_declarations(SafeFile.read(record.path).to_s)
|
|
173
|
+
next if pairs.empty?
|
|
174
|
+
|
|
175
|
+
records << [ record, pairs ]
|
|
176
|
+
pairs.each { |name, _| declared << qualified(record.path_name, name) }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
[ found, declared ]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# [name, superclass] of each `class X < Y` that starts its line. A `^` anchor
|
|
183
|
+
# makes Onigmo try every offset, ten times slower over a service tree.
|
|
184
|
+
def class_declarations(source)
|
|
185
|
+
pairs = []
|
|
186
|
+
source.scan(CLASS_WITH_SUPERCLASS) do |name, base|
|
|
187
|
+
start = Regexp.last_match.begin(0)
|
|
188
|
+
line_start = start.zero? ? 0 : (source.rindex("\n", start - 1) || -1) + 1
|
|
189
|
+
pairs << [ name, base ] if source[line_start...start].match?(/\A[^\S\n]*\z/)
|
|
190
|
+
end
|
|
191
|
+
pairs
|
|
50
192
|
end
|
|
51
193
|
|
|
52
|
-
private_class_method :scan
|
|
194
|
+
private_class_method :scan, :scan_dir, :ruby_files, :walk_dir, :within?, :extra_model_candidates, :extra_model_declarations, :class_declarations, :lookup, :qualified
|
|
53
195
|
|
|
54
|
-
|
|
55
|
-
|
|
196
|
+
# `kind: :models` reads model_paths: what model_details lists, not only app/models.
|
|
197
|
+
def each(root, kind:, skip_concerns: true, base_model: nil, &block)
|
|
198
|
+
return enum_for(:each, root, kind: kind, skip_concerns: skip_concerns, base_model: base_model) unless block
|
|
56
199
|
|
|
57
|
-
|
|
200
|
+
read = lambda do |record|
|
|
58
201
|
source = SafeFile.read(record.path) or next
|
|
59
|
-
|
|
202
|
+
block.call(record.with(source: source))
|
|
60
203
|
end
|
|
204
|
+
return paths(root, kind: kind, skip_concerns: skip_concerns, &read) unless kind == :models
|
|
205
|
+
|
|
206
|
+
model_paths(root, base_model: base_model) { |record| read.call(record) unless skip_concerns && record.path_name.start_with?("Concerns::") }
|
|
61
207
|
end
|
|
62
208
|
|
|
63
209
|
# The eager form: reads and parses every file for its declared name.
|
|
64
210
|
# A caller that names only some files resolves DeclaredConstant itself.
|
|
65
|
-
def classes(root, kind:)
|
|
66
|
-
each(root, kind: kind).filter_map do |record|
|
|
67
|
-
next unless DeclaredConstant.declares_class?(record.source)
|
|
211
|
+
def classes(root, kind:, base_model: nil)
|
|
212
|
+
each(root, kind: kind, base_model: base_model).filter_map do |record|
|
|
213
|
+
next unless DeclaredConstant.declares_class?(record.source, path_name: record.path_name)
|
|
68
214
|
|
|
69
215
|
[ DeclaredConstant.resolve(record.source, record.path_name), record ]
|
|
70
216
|
end
|
|
@@ -74,16 +220,18 @@ module RailsAiContext
|
|
|
74
220
|
# to a real path the root does not contain; the unresolved path is
|
|
75
221
|
# still the one the app spells.
|
|
76
222
|
def relative_file(path, real, root, real_root)
|
|
77
|
-
real_prefix = real_root
|
|
223
|
+
real_prefix = SafePath.dir_prefix(real_root)
|
|
78
224
|
return real.delete_prefix(real_prefix) if real.start_with?(real_prefix)
|
|
79
225
|
|
|
80
|
-
path.delete_prefix(root
|
|
226
|
+
path.delete_prefix(SafePath.dir_prefix(root))
|
|
81
227
|
end
|
|
82
228
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
229
|
+
# Either spelling may land inside the root (only one does for a symlinked
|
|
230
|
+
# pack), but a spelled ".." can climb out through a symlink, so it needs the real one.
|
|
85
231
|
def under_root?(path, real, root, real_root)
|
|
86
|
-
|
|
232
|
+
return true if SafePath.contained?(real, real_root)
|
|
233
|
+
|
|
234
|
+
!path.split(File::SEPARATOR).include?("..") && File.expand_path(path).start_with?(SafePath.dir_prefix(root))
|
|
87
235
|
end
|
|
88
236
|
end
|
|
89
237
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
# Rails 8 leaves a virtual table and its shadow tables out of connection.tables;
|
|
6
|
+
# Rails 7.x lists them as plain tables and has no virtual_tables, so both are read here.
|
|
7
|
+
module SqliteVirtualTables
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Each virtual table's [module, arguments], read from sqlite_master as Rails 8 does.
|
|
11
|
+
def of(connection)
|
|
12
|
+
connection.select_rows("SELECT name, sql FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL %'").to_h do |name, sql|
|
|
13
|
+
[ name, sql.match(/USING\s+(\w+)\s*(?:\((.*)\))?/im).to_a.drop(1) ]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ponytail: pragma_table_list needs SQLite 3.37+; an older SQLite under Rails 7.x still lists shadow tables.
|
|
18
|
+
def hidden(connection)
|
|
19
|
+
virtual = of(connection).keys
|
|
20
|
+
return virtual if virtual.empty?
|
|
21
|
+
|
|
22
|
+
virtual + connection.select_values("SELECT name FROM pragma_table_list WHERE type = 'shadow'")
|
|
23
|
+
rescue ActiveRecord::StatementInvalid
|
|
24
|
+
virtual
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -144,7 +144,7 @@ module RailsAiContext
|
|
|
144
144
|
# the rails-new home an app's own loader can strip a segment the path keeps.
|
|
145
145
|
def identifier_for(path, js_root)
|
|
146
146
|
parts = path.sub("#{js_root}/", "").split("/")
|
|
147
|
-
index = parts.
|
|
147
|
+
index = parts.index("controllers")
|
|
148
148
|
stem = (index ? parts[(index + 1)..] : parts).join("/")
|
|
149
149
|
.sub(/(?:_controller|\.controller)\.\w+\z/, "")
|
|
150
150
|
[ stem.gsub("/", "--").tr("_", "-"), !default_home?(path, js_root) ]
|
|
@@ -159,7 +159,7 @@ module RailsAiContext
|
|
|
159
159
|
# controller left without a free name is qualified by its code root and marked a guess.
|
|
160
160
|
def resolve_names(paths, referenced, registered = {}, root: nil, loader_dirs: [])
|
|
161
161
|
derived = paths.map { |path, js_root| [ path, js_root, identifier_for(path, js_root) ] }
|
|
162
|
-
.sort_by { |path, js_root,
|
|
162
|
+
.sort_by { |path, js_root, (_name, guess)| [ code_root_of(js_root).count("/"), guess ? 1 : 0, path ] }
|
|
163
163
|
claimed = derived.map { |_path, _js_root, (name, _guess)| name }.to_set
|
|
164
164
|
taken = Set.new
|
|
165
165
|
resolved = {}
|
|
@@ -238,7 +238,7 @@ module RailsAiContext
|
|
|
238
238
|
|
|
239
239
|
def controllers_home(path, js_root)
|
|
240
240
|
parts = path.delete_prefix("#{js_root}/").split("/")
|
|
241
|
-
index = parts.
|
|
241
|
+
index = parts.index("controllers") or return nil
|
|
242
242
|
File.join(js_root, *parts.first(index + 1))
|
|
243
243
|
end
|
|
244
244
|
|
|
@@ -255,11 +255,20 @@ module RailsAiContext
|
|
|
255
255
|
end
|
|
256
256
|
end
|
|
257
257
|
|
|
258
|
+
def js_dir_of(js_root)
|
|
259
|
+
JS_ROOTS.find { |dir| js_root == dir || js_root.end_with?("/#{dir}") }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The code root a JS root belongs to: `frontend` and `app/javascript` share one.
|
|
263
|
+
def code_root_of(js_root)
|
|
264
|
+
js_root.delete_suffix(js_dir_of(js_root).to_s).chomp("/")
|
|
265
|
+
end
|
|
266
|
+
|
|
258
267
|
# Names for a controller whose derived one is taken: prefixed by more and more
|
|
259
268
|
# of its code root (`chat--foo`, `plugins--chat--foo`), then the whole path.
|
|
260
269
|
def qualified_names(name, js_root, root)
|
|
261
270
|
relative = root ? js_root.delete_prefix("#{root}/") : js_root
|
|
262
|
-
js_dir =
|
|
271
|
+
js_dir = js_dir_of(relative)
|
|
263
272
|
segments = (js_dir ? relative.delete_suffix(js_dir).chomp("/") : relative).split("/").reject(&:empty?)
|
|
264
273
|
segments = [ js_dir.tr("/", "-") ] if segments.empty? && js_dir
|
|
265
274
|
(1..segments.size).map { |n| "#{segments.last(n).join('--')}--#{name}" } +
|
|
@@ -300,7 +309,9 @@ module RailsAiContext
|
|
|
300
309
|
# Every template or Ruby file an identifier can be written in: the one
|
|
301
310
|
# file set that naming and the "used in" lookup both read.
|
|
302
311
|
def template_files(root)
|
|
303
|
-
TEMPLATE_KINDS.flat_map { |kind| PathResolver.dirs_for(root, kind) }.uniq
|
|
312
|
+
dirs = TEMPLATE_KINDS.flat_map { |kind| PathResolver.dirs_for(root, kind) }.uniq
|
|
313
|
+
dirs += PathResolver.view_dirs(root).reject { |views| dirs.any? { |dir| views == dir || views.start_with?("#{dir}/") } }
|
|
314
|
+
dirs.flat_map { |dir|
|
|
304
315
|
FileWalk.each_file(dir, skip: SKIP_DIRS).select { |path| path.match?(TEMPLATE_FILE) }.sort
|
|
305
316
|
}.uniq
|
|
306
317
|
end
|