rigortype 0.4.4 → 0.4.6
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/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -159,13 +159,18 @@ module Rigor
|
|
|
159
159
|
|
|
160
160
|
# Project-relative paths, the spelling `#template_units_for_file` is handed and the spelling the
|
|
161
161
|
# compiled cache is keyed by.
|
|
162
|
+
#
|
|
163
|
+
# #1803 — listed through the boundary's `#glob`, which records each root's membership: a helper
|
|
164
|
+
# added under `app/helpers`, or a template under a `view_search_paths:` root the template globs do
|
|
165
|
+
# not claim, is analysed by nobody, and without the row the run-result caches kept serving the
|
|
166
|
+
# index built before it appeared.
|
|
162
167
|
def self.source_files(io_boundary, roots, pattern)
|
|
163
168
|
root_prefix = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
164
169
|
roots.flat_map do |root|
|
|
165
170
|
absolute = File.expand_path(root)
|
|
166
171
|
next [] unless io_boundary.directory?(absolute)
|
|
167
172
|
|
|
168
|
-
|
|
173
|
+
io_boundary.glob(absolute, "**/#{pattern}").map { |path| path.delete_prefix(root_prefix) }
|
|
169
174
|
end
|
|
170
175
|
end
|
|
171
176
|
|
|
@@ -146,6 +146,19 @@ module Rigor
|
|
|
146
146
|
# `sig/action_view.rbs` so the name resolves, open so the helper surface stays lenient.
|
|
147
147
|
"ActionView::Base"
|
|
148
148
|
],
|
|
149
|
+
# ADR-125 (#575) — Rails instantiates a fresh controller per request and runs exactly one action on
|
|
150
|
+
# it, so ivar writes in two different public actions are never live together and
|
|
151
|
+
# `def.ivar-write-mismatch` stops comparing them. Neither root has a signature (see `sig/action_controller.rbs`
|
|
152
|
+
# for why); the rule matches an undeclared superclass by its written name.
|
|
153
|
+
per_invocation_classes: ["ActionController::Base", "ActionController::API"],
|
|
154
|
+
# A public method these macros name is a callback or a view helper, not an action: it runs inside
|
|
155
|
+
# whichever action triggers it, so its writes stay compared with every action's (#575's A1 decision).
|
|
156
|
+
per_invocation_non_action_macros: %i[
|
|
157
|
+
before_action prepend_before_action append_before_action skip_before_action
|
|
158
|
+
after_action prepend_after_action append_after_action skip_after_action
|
|
159
|
+
around_action prepend_around_action append_around_action skip_around_action
|
|
160
|
+
helper_method
|
|
161
|
+
],
|
|
149
162
|
# ADR-103 WD10 / WD14 (#387) — see {Effects} for what each row is and why.
|
|
150
163
|
effect_root: "rails",
|
|
151
164
|
effect_labels: %w[
|
|
@@ -127,7 +127,7 @@ module Rigor
|
|
|
127
127
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
128
128
|
next [] unless @io_boundary.directory?(absolute)
|
|
129
129
|
|
|
130
|
-
Dir.glob(File.join(absolute,
|
|
130
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -146,11 +146,12 @@ module Rigor
|
|
|
146
146
|
|
|
147
147
|
# ADR-45 WD1b (#613) — `config/application.rb` is probed at a fixed path, so its ABSENCE is what
|
|
148
148
|
# shapes "no adapter" on a project that has none yet; the probe goes through the boundary to record
|
|
149
|
-
# that. The
|
|
150
|
-
# (immediately superseded) presence row.
|
|
149
|
+
# that. The `config/environments/*.rb` listing goes through `#glob` (#1803), whose row records the
|
|
150
|
+
# directory's membership; each listed entry only picks up the (immediately superseded) presence row.
|
|
151
151
|
def config_files(io_boundary, root)
|
|
152
152
|
application = File.join(root, CONFIG_PATHS.first)
|
|
153
|
-
|
|
153
|
+
environments_dir = File.join(root, CONFIG_PATHS.last)
|
|
154
|
+
environments = io_boundary.glob(environments_dir, "*.rb")
|
|
154
155
|
([application] + environments).select { |path| io_boundary.file?(path) }
|
|
155
156
|
rescue StandardError
|
|
156
157
|
[]
|
|
@@ -92,7 +92,7 @@ module Rigor
|
|
|
92
92
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
93
93
|
next [] unless @io_boundary.directory?(absolute)
|
|
94
94
|
|
|
95
|
-
Dir.glob(File.join(absolute,
|
|
95
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -97,8 +97,10 @@ module Rigor
|
|
|
97
97
|
|
|
98
98
|
# Cached separately from `:job_index` because the two invalidate on different files: editing a
|
|
99
99
|
# schedule changes which jobs are reached without touching `app/jobs` at all. The `watch:` roots the
|
|
100
|
-
# glob at the working directory so that CREATING a schedule file — not just editing one — is seen.
|
|
101
|
-
|
|
100
|
+
# glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
|
|
101
|
+
# configured path is escaped to a literal name, so `recurring[1].yml` is watched as that file.
|
|
102
|
+
producer :recurring_jobs,
|
|
103
|
+
watch: -> { [[".", *@recurring_paths.map { |path| Rigor::RootedGlob.literal(path) }]] } do |_params|
|
|
102
104
|
RecurringScan.new(
|
|
103
105
|
io_boundary: io_boundary,
|
|
104
106
|
recurring_paths: @recurring_paths
|
|
@@ -375,7 +375,7 @@ module Rigor
|
|
|
375
375
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
376
376
|
next [] unless @io_boundary.directory?(absolute)
|
|
377
377
|
|
|
378
|
-
Dir.glob(File.join(absolute,
|
|
378
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
379
379
|
end
|
|
380
380
|
end
|
|
381
381
|
|
|
@@ -69,7 +69,7 @@ module Rigor
|
|
|
69
69
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
70
70
|
next [] unless @io_boundary.directory?(absolute)
|
|
71
71
|
|
|
72
|
-
Dir.glob(File.join(absolute,
|
|
72
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -65,7 +65,7 @@ module Rigor
|
|
|
65
65
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
66
66
|
next [] unless @io_boundary.directory?(absolute)
|
|
67
67
|
|
|
68
|
-
Dir.glob(File.join(absolute,
|
|
68
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -33,9 +33,11 @@ module Rigor
|
|
|
33
33
|
# - `:authenticatable` — always-included base module
|
|
34
34
|
#
|
|
35
35
|
# The substrate's pre-pass scanner consults each module's RBS via `Environment::RbsLoader#instance_definition`
|
|
36
|
-
# to enumerate method names
|
|
37
|
-
#
|
|
38
|
-
#
|
|
36
|
+
# to enumerate method names, so it names only what the project's RBS declares. The `devise` gem ships no
|
|
37
|
+
# RBS and Rigor bundles none for `Devise::Models::*`: a project supplies those signatures (`signature_paths:`
|
|
38
|
+
# or an `rbs collection`), and only then do `valid_password?`, `send_reset_password_instructions`, etc.
|
|
39
|
+
# resolve through the synthetic-method tier. A name the model defines itself, or a project module it
|
|
40
|
+
# includes or prepends defines, is left to the project's definition.
|
|
39
41
|
#
|
|
40
42
|
# ## Precision tier
|
|
41
43
|
#
|
|
@@ -109,7 +109,7 @@ module Rigor
|
|
|
109
109
|
def scannable_paths
|
|
110
110
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
111
111
|
if io_boundary.directory?(entry)
|
|
112
|
-
Dir.glob(File.join(entry,
|
|
112
|
+
Dir.glob("**/*.rb", base: entry).map { |path| File.join(entry, path) }
|
|
113
113
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
114
114
|
[entry]
|
|
115
115
|
else
|
|
@@ -120,16 +120,17 @@ module Rigor
|
|
|
120
120
|
|
|
121
121
|
# The `watch:` glob tuples covering the scan's entire input — one `[root, pattern]` per project `paths:`
|
|
122
122
|
# entry. A directory entry watches its whole `**/*.rb` subtree; a single-file `.rb` entry watches
|
|
123
|
-
# exactly that file (`[dir, basename]`
|
|
124
|
-
# `Plugin::Base#watch_glob_entries`, so a relative CLI path
|
|
125
|
-
# tree from the project root on the next run. Co-extensive
|
|
126
|
-
# would read is watched, so any edit / addition / removal
|
|
123
|
+
# exactly that file (`[dir, basename]`, with the basename escaped so `[`, `{`, `*` or `?` match literally).
|
|
124
|
+
# Roots are expanded to absolute paths by `Plugin::Base#watch_glob_entries`, so a relative CLI path
|
|
125
|
+
# (`app/models`) re-validates against the same tree from the project root on the next run. Co-extensive
|
|
126
|
+
# with {#scannable_paths}: every `.rb` the scan would read is watched, so any edit / addition / removal
|
|
127
|
+
# under those paths invalidates the cache.
|
|
127
128
|
def alias_watch_globs
|
|
128
129
|
services.configuration.paths.filter_map do |entry|
|
|
129
130
|
if io_boundary.directory?(entry)
|
|
130
131
|
[entry, "**/*.rb"]
|
|
131
132
|
elsif io_boundary.file?(entry) && entry.end_with?(".rb")
|
|
132
|
-
[File.dirname(entry), File.basename(entry)]
|
|
133
|
+
[File.dirname(entry), Rigor::RootedGlob.literal(File.basename(entry))]
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
136
|
end
|
|
@@ -11,6 +11,8 @@ module Rigor
|
|
|
11
11
|
# carried no row at all and was served after any edit, an analysed file's included, until `--no-cache`.
|
|
12
12
|
class Discoverer
|
|
13
13
|
RUBY_GLOB = "**/*.rb"
|
|
14
|
+
RUBY_FILE = "*.rb"
|
|
15
|
+
SKIPPED_DIRECTORIES = %w[vendor .git].freeze
|
|
14
16
|
|
|
15
17
|
def self.discover(root:, io:)
|
|
16
18
|
new(root: root, io: io).discover
|
|
@@ -67,20 +69,47 @@ module Rigor
|
|
|
67
69
|
|
|
68
70
|
private
|
|
69
71
|
|
|
72
|
+
# #1959 — one `:kinds` listing row over the root's top-level entries, then one `:names` `**/*.rb` row per
|
|
73
|
+
# real directory among them other than `vendor/`. A single `**/*.rb` row over the root re-walked an in-tree
|
|
74
|
+
# `vendor/bundle` on every warm validation and read stale on every `bundle install`. The listing carries
|
|
75
|
+
# each entry's kind and not just its name: the walk descends a directory but not a symlinked one (`**` never
|
|
76
|
+
# followed it), so a file `gen` that becomes a directory `gen/`, or a symlink that becomes a real
|
|
77
|
+
# directory, changes what it reads while every name stays put.
|
|
78
|
+
#
|
|
79
|
+
# The answer is `Dir.glob("**/*.rb")` minus the skipped paths and minus every match that is not a regular
|
|
80
|
+
# file (#2113), in its order: entry by entry, a top-level name matching `*.rb`, then that directory's
|
|
81
|
+
# matches. A directory so named is walked, never read. The boundary answers absolute paths; they are spelled
|
|
82
|
+
# back under `@root` as `Dir.glob` spells them.
|
|
70
83
|
def ruby_files
|
|
71
84
|
return [] unless @root && @io.directory?(@root.to_s)
|
|
72
85
|
|
|
73
|
-
boundary_glob.reject { |p| p.include?("/vendor/") || p.include?("/.git/") }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# One `:names` `**/*.rb` row over the root, `vendor/` included: a per-entry walk that skipped it recorded
|
|
77
|
-
# names only, so a top-level entry that changed kind under the same name went unseen. The boundary
|
|
78
|
-
# answers absolute paths; they are spelled back under `@root` as `Dir.glob` spells them, so the filter
|
|
79
|
-
# above sees the same strings it always did.
|
|
80
|
-
def boundary_glob
|
|
81
86
|
root = @root.to_s
|
|
82
87
|
prefix = "#{File.absolute_path(root)}/"
|
|
83
|
-
@io.
|
|
88
|
+
@io.list_directory_kinds(root).flat_map do |path, kind|
|
|
89
|
+
name = path.delete_prefix(prefix)
|
|
90
|
+
own = File.fnmatch?(RUBY_FILE, name) && readable_file?(path, kind) ? [File.join(root, name)] : []
|
|
91
|
+
next own unless kind == :directory && !SKIPPED_DIRECTORIES.include?(name)
|
|
92
|
+
|
|
93
|
+
own + @io.glob(path, RUBY_GLOB).filter_map do |nested|
|
|
94
|
+
relative = nested.delete_prefix(prefix)
|
|
95
|
+
File.join(root, relative) if !skipped?(relative) && @io.file?(nested)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# #2113 — `*.rb` names a directory too, and a fifo, socket or device can carry the name. Only a regular file
|
|
101
|
+
# is read: `read_file` on a directory raises `EISDIR` and aborts the whole walk, and on a fifo it blocks
|
|
102
|
+
# until a writer appears. A top-level entry is judged by its `lstat` kind, and a symlink by what it
|
|
103
|
+
# resolves to; a nested match is judged by `file?` (which follows a symlink as `File.file?` does).
|
|
104
|
+
def readable_file?(path, kind)
|
|
105
|
+
kind == :file || (kind == :symlink && @io.file?(path))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# #1956 — the skip reads the path relative to the root, a `vendor` or `.git` directory segment at any
|
|
109
|
+
# depth under it. Matching `/vendor/` in the whole path also dropped every file of a project that itself
|
|
110
|
+
# lives under some `…/vendor/…` directory, and the catalog came out empty.
|
|
111
|
+
def skipped?(relative)
|
|
112
|
+
relative.split("/")[0...-1].intersect?(SKIPPED_DIRECTORIES)
|
|
84
113
|
end
|
|
85
114
|
|
|
86
115
|
# The glob's rows record which files match; this read's row carries an edit.
|
|
@@ -26,7 +26,7 @@ module Rigor
|
|
|
26
26
|
def extconf_uses_ffx?(root, io = nil)
|
|
27
27
|
return false if root.nil?
|
|
28
28
|
|
|
29
|
-
extconfs = io ? io.glob(root.to_s, EXTCONF_GLOB) : Dir.glob(File.join(root.to_s,
|
|
29
|
+
extconfs = io ? io.glob(root.to_s, EXTCONF_GLOB) : Dir.glob(EXTCONF_GLOB, base: root.to_s).map { |path| File.join(root.to_s, path) }
|
|
30
30
|
extconfs.any? do |extconf|
|
|
31
31
|
read_text(extconf, io).include?("FFX.create_makefile")
|
|
32
32
|
rescue SystemCallError
|
|
@@ -72,7 +72,7 @@ module Rigor
|
|
|
72
72
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
73
73
|
next [] unless @io_boundary.directory?(absolute)
|
|
74
74
|
|
|
75
|
-
Dir.glob(File.join(absolute,
|
|
75
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -60,7 +60,7 @@ module Rigor
|
|
|
60
60
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
61
61
|
next [] unless @io_boundary.directory?(absolute)
|
|
62
62
|
|
|
63
|
-
Dir.glob(File.join(absolute,
|
|
63
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -34,12 +34,17 @@ module Rigor
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
attr_reader :entries, :locales
|
|
37
|
+
attr_reader :entries, :locales, :load_errors
|
|
38
38
|
|
|
39
39
|
# @param locales — all locale names that contributed at least one key.
|
|
40
|
-
|
|
40
|
+
# @param load_errors — the {LocaleLoader::LoadError}s of the files that did not parse (#1954). They
|
|
41
|
+
# ride in the index, not beside it, because the index is the cached producer value: a warm run
|
|
42
|
+
# serves it without re-reading a single locale file, so an error kept anywhere else is lost exactly
|
|
43
|
+
# when the cache hits.
|
|
44
|
+
def initialize(entries, locales:, load_errors: [])
|
|
41
45
|
@entries = entries.freeze
|
|
42
46
|
@locales = locales.dup.freeze
|
|
47
|
+
@load_errors = load_errors.map(&:freeze).freeze
|
|
43
48
|
@by_key = entries.to_h { |e| [e.dotted_key, e] }.freeze
|
|
44
49
|
freeze
|
|
45
50
|
end
|
|
@@ -15,9 +15,9 @@ module Rigor
|
|
|
15
15
|
# flattened into dotted keys (`users.welcome`, `errors.messages.blank`, …). For each leaf string,
|
|
16
16
|
# `%{var}` placeholders are extracted via a simple regex.
|
|
17
17
|
#
|
|
18
|
-
# Files that fail to parse are skipped
|
|
19
|
-
# error
|
|
20
|
-
# locale-keyed by convention.
|
|
18
|
+
# Files that fail to parse are skipped and recorded in the returned index's `load_errors`, which the
|
|
19
|
+
# plugin discloses as a run-scoped load-error row. Non-Hash YAML roots (e.g. a top-level sequence) are
|
|
20
|
+
# also skipped — the format is locale-keyed by convention.
|
|
21
21
|
class LocaleLoader
|
|
22
22
|
PLACEHOLDER_RE = /%\{(?<name>[^}]+)\}/
|
|
23
23
|
|
|
@@ -66,7 +66,7 @@ module Rigor
|
|
|
66
66
|
value_kinds: per_key_kinds[dotted_key].freeze
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
|
-
LocaleIndex.new(entries, locales: locales.to_a.sort)
|
|
69
|
+
LocaleIndex.new(entries, locales: locales.to_a.sort, load_errors: @load_errors)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
private
|
|
@@ -90,7 +90,7 @@ module Rigor
|
|
|
90
90
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
91
91
|
next [] unless @io_boundary.directory?(absolute)
|
|
92
92
|
|
|
93
|
-
Dir.glob(File.join(absolute,
|
|
93
|
+
Dir.glob("**/*.{yml,yaml}", base: absolute).map { |path| File.join(absolute, path) }
|
|
94
94
|
end.sort
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -57,7 +57,11 @@ module Rigor
|
|
|
57
57
|
target_gems: ["i18n"],
|
|
58
58
|
# Bumped 2026-06-23 — view template lazy-key scanning (`t('.key')` inside ERB / Haml / Slim under
|
|
59
59
|
# `view_search_paths`).
|
|
60
|
-
|
|
60
|
+
# Bumped 2026-10-17 (#1954) — the cached `:locale_index` value now carries the YAML load errors. A
|
|
61
|
+
# belt-and-braces key change: a released build already drops pre-change entries with the cache ABI
|
|
62
|
+
# (`Rigor::VERSION`) and a checkout with its engine-source identity, and the read below tolerates
|
|
63
|
+
# an index without errors either way.
|
|
64
|
+
version: "0.3.1",
|
|
61
65
|
description: "Validates I18n `t(key)` calls against `config/locales/*.yml`.",
|
|
62
66
|
config_schema: {
|
|
63
67
|
"locale_search_paths" => { kind: :array, default: ["config/locales"] },
|
|
@@ -72,17 +76,12 @@ module Rigor
|
|
|
72
76
|
)
|
|
73
77
|
|
|
74
78
|
# `watch:` covers every `.yml` / `.yaml` file under the locale search paths so the cache invalidates
|
|
75
|
-
# when locale files are added, removed, or edited (ADR-60 WD3).
|
|
76
|
-
#
|
|
77
|
-
#
|
|
79
|
+
# when locale files are added, removed, or edited (ADR-60 WD3). The YAML load errors travel inside the
|
|
80
|
+
# returned index (`LocaleIndex#load_errors`), not in an instance variable the block sets (#1954): a
|
|
81
|
+
# warm producer-cache hit skips the block, so a block-side capture lost the load-error row on every
|
|
82
|
+
# run whose result was not itself served from the run-result cache.
|
|
78
83
|
producer :locale_index, watch: -> { [[@locale_search_paths, "**/*.yml", "**/*.yaml"]] } do |_params|
|
|
79
|
-
|
|
80
|
-
io_boundary: io_boundary,
|
|
81
|
-
search_paths: @locale_search_paths
|
|
82
|
-
)
|
|
83
|
-
index = loader.load
|
|
84
|
-
@load_errors = loader.load_errors
|
|
85
|
-
index
|
|
84
|
+
LocaleLoader.new(io_boundary: io_boundary, search_paths: @locale_search_paths).load
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
# Scans view templates under `view_search_paths` for lazy `t('.key')` / `I18n.translate('.key')`
|
|
@@ -102,7 +101,6 @@ module Rigor
|
|
|
102
101
|
@locale_search_paths = Array(config.fetch("locale_search_paths")).map(&:to_s)
|
|
103
102
|
@view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
|
|
104
103
|
@configured_locales = Array(config.fetch("configured_locales")).map(&:to_s)
|
|
105
|
-
@load_errors = []
|
|
106
104
|
end
|
|
107
105
|
|
|
108
106
|
# Returns no rows of its own: every file-level outcome here is run-scoped. The YAML load errors and the
|
|
@@ -114,8 +112,11 @@ module Rigor
|
|
|
114
112
|
# are lazily loaded + memoised by `producer_value`, so the repeat registrations are no-ops.
|
|
115
113
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
116
114
|
index = producer_value(:locale_index)
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
if index
|
|
116
|
+
disclose_load_errors(Array(index.load_errors))
|
|
117
|
+
elsif producer_error(:locale_index)
|
|
118
|
+
disclose_locale_runtime_error
|
|
119
|
+
end
|
|
119
120
|
view_diags = producer_value(:view_diagnostics) || []
|
|
120
121
|
if (view_err = producer_error(:view_diagnostics))
|
|
121
122
|
disclose_view_runtime_error(view_err)
|
|
@@ -175,7 +176,7 @@ module Rigor
|
|
|
175
176
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
176
177
|
next [] unless io_boundary.directory?(absolute)
|
|
177
178
|
|
|
178
|
-
Dir.glob(
|
|
179
|
+
Dir.glob("**/*.{erb,haml,slim}", base: absolute).map { |path| File.join(absolute, path) }
|
|
179
180
|
end.sort
|
|
180
181
|
end
|
|
181
182
|
|
|
@@ -209,8 +210,8 @@ module Rigor
|
|
|
209
210
|
# be pinned to the first analysed Ruby file — once per plugin INSTANCE, so once per fork-pool worker
|
|
210
211
|
# (#1051). They go through the run-scoped channel instead: keyed by the locale path that failed, so a
|
|
211
212
|
# project with two broken YAML files still discloses both, each exactly once, at `.rigor.yml:1:1`.
|
|
212
|
-
def disclose_load_errors
|
|
213
|
-
|
|
213
|
+
def disclose_load_errors(load_errors)
|
|
214
|
+
load_errors.each do |err|
|
|
214
215
|
disclose_once(
|
|
215
216
|
"locale-parse:#{err.path}",
|
|
216
217
|
message: "rigor-rails-i18n: failed to parse `#{err.path}`: #{err.message}",
|
|
@@ -168,7 +168,7 @@ module Rigor
|
|
|
168
168
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
169
169
|
next unless io_boundary.directory?(absolute)
|
|
170
170
|
|
|
171
|
-
Dir.glob(File.join(absolute,
|
|
171
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }.each(&)
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
@@ -35,7 +35,8 @@ module Rigor
|
|
|
35
35
|
# LocalVariableReadNode. Composite receivers (`expect(foo.bar)`, `expect { ... }.to raise_error`)
|
|
36
36
|
# fall through.
|
|
37
37
|
# 3. The matcher is one of the recognised forms above (`be_a` / `be_kind_of` / `be_instance_of` /
|
|
38
|
-
# `be_an_instance_of` / `be_nil` / `eq(literal)` / `eql(literal)` / `match(/regex/)
|
|
38
|
+
# `be_an_instance_of` / `be_nil` / `eq(literal)` / `eql(literal)` / `match(/regex/)`, the last a meet
|
|
39
|
+
# with the local's members; see `regex_match_type`).
|
|
39
40
|
#
|
|
40
41
|
# ## Equality is the local's own method
|
|
41
42
|
#
|
|
@@ -152,10 +153,7 @@ module Rigor
|
|
|
152
153
|
when :eq, :eql
|
|
153
154
|
equality_type_for_literal_arg(matcher, target_local, negative: negative, scope: scope)
|
|
154
155
|
when :match
|
|
155
|
-
|
|
156
|
-
# through — the broader matcher dispatch needs the receiver to be a String, but we can only
|
|
157
|
-
# assert that for a literal regex.
|
|
158
|
-
string_type_for_regex_arg(matcher)
|
|
156
|
+
regex_match_type(matcher, target_local, negative: negative, scope: scope)
|
|
159
157
|
end
|
|
160
158
|
end
|
|
161
159
|
|
|
@@ -293,15 +291,108 @@ module Rigor
|
|
|
293
291
|
end
|
|
294
292
|
end
|
|
295
293
|
|
|
296
|
-
|
|
294
|
+
# --- `match(/regex/)` (#2033) ---
|
|
295
|
+
#
|
|
296
|
+
# RSpec's `match` passes when `expected === actual`, when `actual == expected`, or when `actual` responds to
|
|
297
|
+
# `match` and `actual.match(expected)` is truthy. `Regexp#===` matches a Symbol by its name, so a Symbol
|
|
298
|
+
# passes too, and keeps being a Symbol. The positive fact is therefore a meet with the local's members, as
|
|
299
|
+
# `eq`'s is: a String or Symbol member is kept as it is, and a member of a core class that has no `match` or
|
|
300
|
+
# `to_str` and whose `==` never equals a Regexp (`nil`, `true`, `false`, the numerics) is dropped, unless the
|
|
301
|
+
# project defines one of those methods on it or a core ancestor. An `Object`, `BasicObject`, `Kernel` or
|
|
302
|
+
# `Comparable` member reads `String | Symbol` ({#module_match_member}); any other module means no fact. Any other member (a user class, `Dynamic`) may pass, and
|
|
303
|
+
# the analyzer emits no fact; so does a subject none of whose members can pass (a `nil` local stays `nil`). The negative form emits none either: a String that does not match passes
|
|
304
|
+
# `not_to match`, so nothing leaves the local.
|
|
305
|
+
REGEX_MATCH_KEPT_CLASSES = %w[String Symbol].freeze
|
|
306
|
+
REGEX_MATCH_METHODS = %i[match to_str].freeze
|
|
307
|
+
Ractor.make_shareable(REGEX_MATCH_KEPT_CLASSES)
|
|
308
|
+
Ractor.make_shareable(REGEX_MATCH_METHODS)
|
|
309
|
+
private_constant :REGEX_MATCH_KEPT_CLASSES, :REGEX_MATCH_METHODS
|
|
310
|
+
|
|
311
|
+
def regex_match_type(matcher, target_local, negative:, scope:)
|
|
312
|
+
return nil if negative || scope.nil?
|
|
313
|
+
return nil unless regex_literal_arg?(matcher)
|
|
314
|
+
|
|
315
|
+
current = scope.local(target_local)
|
|
316
|
+
return nil if current.nil?
|
|
317
|
+
|
|
318
|
+
members = current.is_a?(Rigor::Type::Union) ? current.members : [current]
|
|
319
|
+
kept = []
|
|
320
|
+
members.each do |member|
|
|
321
|
+
met = regex_match_member(member, scope)
|
|
322
|
+
return nil if met.equal?(UNDECIDED)
|
|
323
|
+
|
|
324
|
+
kept << met unless met.nil?
|
|
325
|
+
end
|
|
326
|
+
kept.empty? ? nil : Rigor::Type::Combinator.union(*kept)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def regex_literal_arg?(matcher)
|
|
297
330
|
args = matcher.arguments&.arguments || []
|
|
298
|
-
|
|
331
|
+
args.size == 1 &&
|
|
332
|
+
(args.first.is_a?(Prism::RegularExpressionNode) || args.first.is_a?(Prism::InterpolatedRegularExpressionNode))
|
|
333
|
+
end
|
|
299
334
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
# The member itself when it may pass, nil when it cannot, UNDECIDED when Rigor cannot tell.
|
|
336
|
+
def regex_match_member(member, scope)
|
|
337
|
+
module_member = module_match_member(member, scope)
|
|
338
|
+
return module_member unless module_member.nil?
|
|
303
339
|
|
|
304
|
-
|
|
340
|
+
class_name = member_class_name(member)
|
|
341
|
+
return UNDECIDED if class_name.nil?
|
|
342
|
+
return member if REGEX_MATCH_KEPT_CLASSES.include?(class_name)
|
|
343
|
+
return member if project_defines?(class_name, :==, scope)
|
|
344
|
+
return member if REGEX_MATCH_METHODS.any? { |name| project_defines_on_chain?(class_name, name, scope) }
|
|
345
|
+
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
OPEN_SURFACE_CLASS_NAMES = %w[Object BasicObject].freeze
|
|
350
|
+
# The modules String's or Symbol's own ancestry includes (`Kernel`, `Comparable`), read from the running Ruby.
|
|
351
|
+
STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
|
|
352
|
+
Ractor.make_shareable(OPEN_SURFACE_CLASS_NAMES)
|
|
353
|
+
Ractor.make_shareable(STRING_SYMBOL_MODULES)
|
|
354
|
+
private_constant :OPEN_SURFACE_CLASS_NAMES, :STRING_SYMBOL_MODULES
|
|
355
|
+
|
|
356
|
+
# The meet for an `Object`, `BasicObject` or module member, or nil for any other member. Its instances may be
|
|
357
|
+
# anything, a `to_str` or `match` object included. A member that declares `match` or `to_str` itself (its
|
|
358
|
+
# signature, a core ancestor, or the project's source) is kept whole. `Object`, `BasicObject` and a module
|
|
359
|
+
# String or Symbol includes otherwise read `String | Symbol`, as the String they most often are, so a String
|
|
360
|
+
# method called after the matcher reports nothing. Any other module is UNDECIDED: a passing value may include
|
|
361
|
+
# it without being a String. (`approval-queue.d/regexp-match-open-surface-subject.md`, shared with #2256.)
|
|
362
|
+
def module_match_member(member, scope)
|
|
363
|
+
return nil unless member.is_a?(Rigor::Type::Nominal) && member.type_args.empty?
|
|
364
|
+
|
|
365
|
+
name = member.class_name
|
|
366
|
+
string_like = OPEN_SURFACE_CLASS_NAMES.include?(name) || STRING_SYMBOL_MODULES.include?(name)
|
|
367
|
+
return nil unless string_like || scope.environment&.rbs_module?(name)
|
|
368
|
+
return member if REGEX_MATCH_METHODS.any? { |method_name| declares_method?(name, method_name, scope) }
|
|
369
|
+
return UNDECIDED unless string_like
|
|
370
|
+
|
|
371
|
+
Rigor::Type::Combinator.union(Rigor::Type::Combinator.nominal_of("String"),
|
|
372
|
+
Rigor::Type::Combinator.nominal_of("Symbol"))
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def declares_method?(name, method_name, scope)
|
|
376
|
+
project_defines_on_chain?(name, method_name, scope) ||
|
|
377
|
+
!Rigor::Reflection.instance_method_definition(name, method_name, scope: scope).nil?
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Each core class's (and `Object` / `BasicObject` / `Kernel`'s) runtime ancestor chain. The project's
|
|
381
|
+
# reopenings are looked up along it by name, since the scope's ancestry walk does not cross core classes:
|
|
382
|
+
# `class Numeric; def to_str` reaches a Float member, `module Kernel; def match` a nil one.
|
|
383
|
+
CORE_ANCESTOR_CHAINS = [*EQUALITY_CORE_CLASSES, Object, BasicObject, Kernel].to_h do |mod|
|
|
384
|
+
[mod.name, mod.ancestors.filter_map(&:name).freeze]
|
|
385
|
+
end.freeze
|
|
386
|
+
Ractor.make_shareable(CORE_ANCESTOR_CHAINS)
|
|
387
|
+
private_constant :CORE_ANCESTOR_CHAINS
|
|
388
|
+
|
|
389
|
+
# Whether the project's source defines `method_name` on `class_name` or an ancestor, core ones included: a
|
|
390
|
+
# `def`, an included module, or a `define_method`.
|
|
391
|
+
def project_defines_on_chain?(class_name, method_name, scope)
|
|
392
|
+
CORE_ANCESTOR_CHAINS.fetch(class_name, [class_name]).any? do |name|
|
|
393
|
+
!scope.user_def_through_ancestors(name, method_name).first.nil? ||
|
|
394
|
+
scope.discovered_method_through_ancestors?(name, method_name, :instance)
|
|
395
|
+
end
|
|
305
396
|
end
|
|
306
397
|
|
|
307
398
|
NO_LITERAL = Object.new.freeze
|
|
@@ -92,7 +92,7 @@ module Rigor
|
|
|
92
92
|
# ADR-45 WD1b (#613) — boundary-probed: a root that appears later invalidates the warm run.
|
|
93
93
|
next [] unless @io_boundary.directory?(absolute)
|
|
94
94
|
|
|
95
|
-
Dir.glob(File.join(absolute,
|
|
95
|
+
Dir.glob("**/*.rb", base: absolute).map { |path| File.join(absolute, path) }
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -97,8 +97,10 @@ module Rigor
|
|
|
97
97
|
|
|
98
98
|
# Cached separately from `:worker_index` because the two invalidate on different files: editing a
|
|
99
99
|
# schedule changes which workers are reached without touching `app/workers` at all. The `watch:` roots
|
|
100
|
-
# the glob at the working directory so that CREATING a schedule file — not just editing one — is seen.
|
|
101
|
-
|
|
100
|
+
# the glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
|
|
101
|
+
# configured path is escaped to a literal name, so `sidekiq[1].yml` is watched as that file.
|
|
102
|
+
producer :scheduled_workers,
|
|
103
|
+
watch: -> { [[".", *@schedule_paths.map { |path| Rigor::RootedGlob.literal(path) }]] } do |_params|
|
|
102
104
|
ScheduleScan.new(
|
|
103
105
|
io_boundary: io_boundary,
|
|
104
106
|
schedule_paths: @schedule_paths
|
|
@@ -29,7 +29,9 @@ module Rigor
|
|
|
29
29
|
# ## Reach
|
|
30
30
|
#
|
|
31
31
|
# All nine class-level HTTP verb methods Sinatra exposes: `get`, `post`, `put`, `delete`, `head`,
|
|
32
|
-
# `options`, `patch`, `link`, `unlink` (`lib/sinatra/base.rb:1531-1553`)
|
|
32
|
+
# `options`, `patch`, `link`, `unlink` (`lib/sinatra/base.rb:1531-1553`), plus the `before` / `after`
|
|
33
|
+
# filters (`add_filter`) and the `error` / `not_found` handlers, whose blocks `compile!` turns into
|
|
34
|
+
# instance methods through the same `generate_method` (issue #1768). Both modular-style subclasses
|
|
33
35
|
# of `Sinatra::Base` and `Sinatra::Application` (classic top-level style, when used via `class App <
|
|
34
36
|
# Sinatra::Application`) match because the receiver constraint accepts every subclass.
|
|
35
37
|
#
|
|
@@ -57,7 +59,7 @@ module Rigor
|
|
|
57
59
|
block_as_methods: [
|
|
58
60
|
Rigor::Plugin::Macro::BlockAsMethod.new(
|
|
59
61
|
receiver_constraint: "Sinatra::Base",
|
|
60
|
-
method_names: %i[get post put delete head options patch link unlink]
|
|
62
|
+
method_names: %i[get post put delete head options patch link unlink before after error not_found]
|
|
61
63
|
)
|
|
62
64
|
]
|
|
63
65
|
)
|
|
@@ -504,8 +504,8 @@ module Rigor
|
|
|
504
504
|
# must not vary by machine / run. `Dir.glob` sorts its results by default on Ruby 3.0+, so the fold
|
|
505
505
|
# order is already stable (the slice-1 comment's "filesystem order" caveat predates that default);
|
|
506
506
|
# this walk relies on it rather than re-sorting.
|
|
507
|
-
Dir.glob(
|
|
508
|
-
harvest_file(canonicalize(
|
|
507
|
+
Dir.glob("**/*#{ext}", base: absolute).each do |relative|
|
|
508
|
+
harvest_file(canonicalize(File.join(absolute, relative)), catalog, sigil_by_path, parse_errors_by_path)
|
|
509
509
|
end
|
|
510
510
|
end
|
|
511
511
|
elsif io_boundary.file?(absolute) && extensions.any? { |ext| absolute.end_with?(ext) }
|