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
data/lib/rigor/lens/members.rb
CHANGED
|
@@ -14,10 +14,13 @@ module Rigor
|
|
|
14
14
|
# @param contracts — `{[owner, scope, name] => ContractReader::Contract or nil}`
|
|
15
15
|
# @param elsewhere — callable `(owner, scope, name) -> [{path, line}]`: other project files defining it
|
|
16
16
|
# @param site_paths — `{site key => absolute path of its file}`, to match an inline contract's buffer
|
|
17
|
-
|
|
17
|
+
# @param defaulted_blocks — `{[owner, scope, name] => {required, type}}`: the block a member's otherwise
|
|
18
|
+
# defaulted inline declaration declares (ContractReader#read_defaulted_block)
|
|
19
|
+
def initialize(sites:, omissions:, contracts:, elsewhere:, site_paths:, defaulted_blocks: {})
|
|
18
20
|
@sites = sites
|
|
19
21
|
@omissions = omissions
|
|
20
22
|
@contracts = contracts
|
|
23
|
+
@defaulted_blocks = defaulted_blocks
|
|
21
24
|
@elsewhere = elsewhere
|
|
22
25
|
@site_paths = site_paths
|
|
23
26
|
end
|
|
@@ -115,11 +118,22 @@ module Rigor
|
|
|
115
118
|
|
|
116
119
|
params, block, head = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
|
|
117
120
|
slot = inferred_return(single, scope)
|
|
118
|
-
|
|
121
|
+
declared = !site.data["alias_of"] && @defaulted_blocks[[site.data["owner"], scope, name]]
|
|
122
|
+
block = declared || block
|
|
123
|
+
block_rbs = block_spelling(block, declared)
|
|
119
124
|
{ "rbs" => "#{head}#{block_rbs} -> #{return_spelling(slot)}", "params" => params, "block" => block,
|
|
120
125
|
"return" => slot }
|
|
121
126
|
end
|
|
122
127
|
|
|
128
|
+
# A block an otherwise defaulted declaration declares reads as declared (`{ (Integer) -> void }`); one the
|
|
129
|
+
# definition merely takes reads as `?{ (?) -> untyped }`.
|
|
130
|
+
def block_spelling(block, declared)
|
|
131
|
+
return "" if block.nil?
|
|
132
|
+
return " ?{ (?) -> untyped }" unless declared
|
|
133
|
+
|
|
134
|
+
" #{'?' unless declared['required']}{ #{declared['type']['type']} }"
|
|
135
|
+
end
|
|
136
|
+
|
|
123
137
|
# An alias whose original Rigor cannot pin to one `def`: nothing about its signature is known.
|
|
124
138
|
def unknown_overload
|
|
125
139
|
{ "rbs" => nil, "params" => nil, "block" => nil, "return" => Slot.unavailable("not_computed") }
|
data/lib/rigor/lens/run.rb
CHANGED
|
@@ -78,6 +78,7 @@ module Rigor
|
|
|
78
78
|
def reset_captures
|
|
79
79
|
@captured = {}
|
|
80
80
|
@contracts = {}
|
|
81
|
+
@defaulted_blocks = {}
|
|
81
82
|
@site_paths = {}
|
|
82
83
|
end
|
|
83
84
|
|
|
@@ -145,18 +146,39 @@ module Rigor
|
|
|
145
146
|
if published
|
|
146
147
|
published_slot(published, scope.environment)
|
|
147
148
|
else
|
|
148
|
-
declared_slot(site, contract_reader(scope.environment).read_constant(name, scope.environment))
|
|
149
|
+
declared_slot(site, contract_reader(scope.environment).read_constant(name, scope.environment), scope)
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
# The rbs-inline plugin declares every constant of a file it reads, typing an unannotated one from its
|
|
153
154
|
# literal (`[]` as `Array[untyped]`), and reads only a `#:` trailing the assignment as its type: an inline
|
|
154
|
-
# declaration is the constant's own only when that is the site's one contract comment.
|
|
155
|
-
|
|
155
|
+
# declaration is the constant's own only when that is the site's one contract comment. Otherwise it is the
|
|
156
|
+
# plugin's default, which `check` reads all the same: that type, `inferred`. A `Class.new` / `Module.new`
|
|
157
|
+
# value is the exception, which `check` types through its own write reading, not the default `untyped`.
|
|
158
|
+
# An inline declaration from another file says nothing about this site: when the name is written in two
|
|
159
|
+
# files, the one the environment kept decides, and this site's own comment may say otherwise.
|
|
160
|
+
def declared_slot(site, declared, scope)
|
|
156
161
|
return Slot.unavailable("not_computed") if declared.nil?
|
|
157
|
-
return
|
|
162
|
+
return declared unless declared["provenance"] == "inline"
|
|
163
|
+
return Slot.unavailable("not_computed") unless own_inline_declaration?(site, scope)
|
|
164
|
+
return declared if trailing_type?(site)
|
|
165
|
+
return Slot.unavailable("not_computed") if anonymous_namespace?(site.node.value)
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
published_slot(scope.environment.constant_for_name(site.data["name"]), scope.environment)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def own_inline_declaration?(site, scope)
|
|
171
|
+
buffer = scope.environment.rbs_loader&.constant_declaration(site.data["name"])&.last
|
|
172
|
+
path = @site_paths.fetch(site.data["key"])
|
|
173
|
+
buffer&.end_with?(":#{path}") || false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def anonymous_namespace?(node)
|
|
177
|
+
return false unless node.is_a?(Prism::Node)
|
|
178
|
+
return true if node.is_a?(Prism::CallNode) && node.name == :new && node.receiver &&
|
|
179
|
+
%w[Class Module].include?(node.receiver.slice.delete_prefix("::"))
|
|
180
|
+
|
|
181
|
+
node.compact_child_nodes.any? { |child| anonymous_namespace?(child) }
|
|
160
182
|
end
|
|
161
183
|
|
|
162
184
|
def trailing_type?(site)
|
|
@@ -166,6 +188,7 @@ module Rigor
|
|
|
166
188
|
end
|
|
167
189
|
|
|
168
190
|
def published_slot(type, environment)
|
|
191
|
+
return Slot.unavailable("not_computed") if type.nil?
|
|
169
192
|
return Slot.dynamic("inferred", nil) if type.is_a?(Type::Dynamic)
|
|
170
193
|
|
|
171
194
|
spelled = speller(environment).call(type, nil)
|
|
@@ -178,7 +201,11 @@ module Rigor
|
|
|
178
201
|
key = [owner, member_scope, name]
|
|
179
202
|
return if @contracts.key?(key)
|
|
180
203
|
|
|
181
|
-
|
|
204
|
+
reader = contract_reader(scope.environment)
|
|
205
|
+
@contracts[key] = reader.read(owner, member_scope.to_sym, name, scope)
|
|
206
|
+
return if @contracts[key]
|
|
207
|
+
|
|
208
|
+
@defaulted_blocks[key] = reader.read_defaulted_block(owner, member_scope.to_sym, name, scope)
|
|
182
209
|
end
|
|
183
210
|
|
|
184
211
|
def target(entry, diagnostics, session)
|
|
@@ -229,7 +256,7 @@ module Rigor
|
|
|
229
256
|
sites = readers.values.flat_map(&:sites)
|
|
230
257
|
omissions = readers.values.flat_map(&:omissions)
|
|
231
258
|
requested = readers.keys.to_set
|
|
232
|
-
Members.new(sites: sites, omissions: omissions, contracts: @contracts,
|
|
259
|
+
Members.new(sites: sites, omissions: omissions, contracts: @contracts, defaulted_blocks: @defaulted_blocks,
|
|
233
260
|
elsewhere: elsewhere(session, requested), site_paths: @site_paths).build
|
|
234
261
|
end
|
|
235
262
|
|
data/lib/rigor/mcp/server.rb
CHANGED
|
@@ -128,8 +128,8 @@ module Rigor
|
|
|
128
128
|
properties: {
|
|
129
129
|
rule: {
|
|
130
130
|
type: "string",
|
|
131
|
-
description: "Rule ID, legacy alias, or family prefix (call, flow, assert, dump,
|
|
132
|
-
"Omit to list every rule."
|
|
131
|
+
description: "Rule ID, legacy alias, or family prefix (call, flow, def, assert, dump, global, " \
|
|
132
|
+
"suppression, static, effect). Omit to list every rule."
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -235,7 +235,7 @@ module Rigor
|
|
|
235
235
|
end
|
|
236
236
|
|
|
237
237
|
def build_argv(name, args) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
238
|
-
effective_config = args["config"] || @config_path
|
|
238
|
+
effective_config = present_config(args["config"]) || @config_path
|
|
239
239
|
|
|
240
240
|
case name
|
|
241
241
|
when "rigor_check"
|
|
@@ -290,6 +290,14 @@ module Rigor
|
|
|
290
290
|
end
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
+
# A per-call `config` that is blank names no file, so the session default applies rather than `--config=`
|
|
294
|
+
# reaching the CLI as an empty path (which the CLI refuses).
|
|
295
|
+
def present_config(value)
|
|
296
|
+
return value unless value.is_a?(String) && value.strip.empty?
|
|
297
|
+
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
293
301
|
def success(id, result)
|
|
294
302
|
{ "jsonrpc" => "2.0", "id" => id, "result" => result }
|
|
295
303
|
end
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -8,11 +8,13 @@ require "prism"
|
|
|
8
8
|
require_relative "manifest"
|
|
9
9
|
require_relative "node_context"
|
|
10
10
|
require_relative "../analysis/diagnostic"
|
|
11
|
+
require_relative "../analysis/run_disclosure_credit"
|
|
11
12
|
require_relative "../cache/engine_source"
|
|
12
13
|
# `producer generation_cap:` defaults to (and validates against) `Cache::Store::UNBOUNDED_GENERATIONS`, and a
|
|
13
14
|
# plugin class body can be evaluated before anything else pulled the cache layer in.
|
|
14
15
|
require_relative "../cache/store"
|
|
15
16
|
require_relative "../source/node_walker"
|
|
17
|
+
require_relative "../rooted_glob"
|
|
16
18
|
|
|
17
19
|
module Rigor
|
|
18
20
|
module Plugin
|
|
@@ -779,11 +781,12 @@ module Rigor
|
|
|
779
781
|
# that differs between workers.
|
|
780
782
|
def disclose_once(key, message:, severity: :info, rule: "load-error")
|
|
781
783
|
id = key.to_s
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
784
|
+
unless run_registration_taken?(id, batch: false)
|
|
785
|
+
@run_disclosures[id] = {
|
|
786
|
+
key: id, message: message.to_s, severity: severity.to_sym, rule: rule.to_s
|
|
787
|
+
}.freeze
|
|
788
|
+
end
|
|
789
|
+
attribute_run_registration(id)
|
|
787
790
|
nil
|
|
788
791
|
end
|
|
789
792
|
|
|
@@ -815,14 +818,15 @@ module Rigor
|
|
|
815
818
|
# raises `ArgumentError`, which the engine reports as this plugin's `runtime-error`.
|
|
816
819
|
def emit_once(key, diagnostics)
|
|
817
820
|
id = key.to_s
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
next row.dup.freeze if row.is_a?(Rigor::Analysis::Diagnostic)
|
|
821
|
+
unless run_registration_taken?(id, batch: true)
|
|
822
|
+
batch = Array(diagnostics).map do |row|
|
|
823
|
+
next row.dup.freeze if row.is_a?(Rigor::Analysis::Diagnostic)
|
|
822
824
|
|
|
823
|
-
|
|
825
|
+
raise ArgumentError, "emit_once expects Rigor::Analysis::Diagnostic rows, got #{row.class}"
|
|
826
|
+
end
|
|
827
|
+
@run_disclosures[id] = { key: id, diagnostics: batch.freeze }.freeze
|
|
824
828
|
end
|
|
825
|
-
|
|
829
|
+
attribute_run_registration(id)
|
|
826
830
|
nil
|
|
827
831
|
end
|
|
828
832
|
|
|
@@ -846,6 +850,18 @@ module Rigor
|
|
|
846
850
|
end
|
|
847
851
|
private :run_registration_taken?
|
|
848
852
|
|
|
853
|
+
# Issue #2020 — hands every call, a repeat of a key already registered included, to the engine's per-file
|
|
854
|
+
# attribution ({Analysis::RunDisclosureCredit}), so an `--incremental` recheck can replay the row for a file
|
|
855
|
+
# it serves from cache. A repeat counts because a discloser memoises the input its notice is about and
|
|
856
|
+
# re-registers the notice from each file's hook. A batch registered empty adds nothing to replay.
|
|
857
|
+
def attribute_run_registration(id)
|
|
858
|
+
record = @run_disclosures[id]
|
|
859
|
+
return if record.nil? || record[:diagnostics]&.empty?
|
|
860
|
+
|
|
861
|
+
Analysis::RunDisclosureCredit.attribute(self, record)
|
|
862
|
+
end
|
|
863
|
+
private :attribute_run_registration
|
|
864
|
+
|
|
849
865
|
# Boilerplate-reduction helper (review §1.3): the "did you mean …?" suggestion every
|
|
850
866
|
# diagnostic-emitting plugin otherwise hand-rolls. Returns the closest of `candidates` to `name` via
|
|
851
867
|
# `DidYouMean::SpellChecker` (the same engine Ruby's own `NoMethodError` hints use), or `nil` when
|
|
@@ -1003,7 +1019,7 @@ module Rigor
|
|
|
1003
1019
|
# plugin code calls `watch:`.
|
|
1004
1020
|
#
|
|
1005
1021
|
# @param roots — search roots (relative to the project root, or absolute paths)
|
|
1006
|
-
# @param patterns — glob suffixes
|
|
1022
|
+
# @param patterns — glob suffixes matched under each root, the root read literally (#1956).
|
|
1007
1023
|
# Multiple patterns union into one descriptor (`"**/*.erb", "**/*.html"` etc.).
|
|
1008
1024
|
def glob_descriptor(roots, *patterns)
|
|
1009
1025
|
files = collect_glob_files(Array(roots), patterns)
|
|
@@ -1163,7 +1179,7 @@ module Rigor
|
|
|
1163
1179
|
absolute = File.absolute_path(root.to_s)
|
|
1164
1180
|
next [] unless File.directory?(absolute)
|
|
1165
1181
|
|
|
1166
|
-
patterns.flat_map { |pattern|
|
|
1182
|
+
patterns.flat_map { |pattern| RootedGlob.glob(absolute, pattern) }
|
|
1167
1183
|
end
|
|
1168
1184
|
matched.uniq.sort.select { |path| File.file?(path) }
|
|
1169
1185
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "access_denied_error"
|
|
6
|
+
require_relative "../rooted_glob"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Plugin
|
|
@@ -33,6 +34,8 @@ module Rigor
|
|
|
33
34
|
# for a plugin that asks "which of these N candidate files exists here?" — rigor-actionpack tries
|
|
34
35
|
# nine template extensions per `render` call — where N per-path probe rows would put thousands of
|
|
35
36
|
# validations on every warm run and one listing row puts one per directory.
|
|
37
|
+
# - `#list_directory_kinds(path)` — the KIND-aware listing (#1959): each entry with its `lstat` kind, and one
|
|
38
|
+
# `:kinds` row that moves when an entry appears, vanishes or changes kind under the same name.
|
|
36
39
|
# - `#open_url(url)` — fetches the URL when the policy permits it (`network_policy: :allowlist` plus an
|
|
37
40
|
# `allowed_url_hosts` match) and raises {AccessDeniedError} otherwise. v0.1.2 ships the allowlist
|
|
38
41
|
# surface; the default project policy still has `network_policy: :disabled` so plugins that want network
|
|
@@ -189,7 +192,7 @@ module Rigor
|
|
|
189
192
|
# @return the absolute paths directly under `path` in `Dir.glob` order; empty when not a directory
|
|
190
193
|
def list_directory(path)
|
|
191
194
|
absolute = File.absolute_path(path.to_s)
|
|
192
|
-
entries =
|
|
195
|
+
entries = RootedGlob.glob(absolute, "*")
|
|
193
196
|
unless @policy.allow_read?(absolute)
|
|
194
197
|
record_refusal(absolute)
|
|
195
198
|
return entries
|
|
@@ -199,6 +202,34 @@ module Rigor
|
|
|
199
202
|
entries
|
|
200
203
|
end
|
|
201
204
|
|
|
205
|
+
# Issue #1959 — the KIND-aware listing. Returns each entry directly under `path` with what it is, a symlink
|
|
206
|
+
# not followed ({Cache::Descriptor::GlobEntry.lstat_kind}: `:file`, `:directory`, `:symlink` or `:other`, and
|
|
207
|
+
# `:absent` for an entry that vanished between the listing and its `lstat`),
|
|
208
|
+
# and records one `:kinds` {Cache::Descriptor::GlobEntry} over `path/*`. The row reads stale when an entry
|
|
209
|
+
# appears, disappears, or keeps its name and changes kind; NOT when an entry's bytes or stat tuple move, nor
|
|
210
|
+
# when anything below a subdirectory changes. It is the row for a walk that decides per top-level entry
|
|
211
|
+
# whether to descend: rigor-ffi skips `vendor/` and does not follow a symlinked directory, so a file `gen`
|
|
212
|
+
# replaced by a directory `gen/`, or a symlink replaced by a real directory, changes what it reads while
|
|
213
|
+
# every name stays put. {#list_directory}'s `:stat` row leaves directories out, and {#glob}'s `:names` row
|
|
214
|
+
# reads names alone.
|
|
215
|
+
#
|
|
216
|
+
# Like {#list_directory}, dot entries are not listed, and an out-of-policy directory is listed truthfully
|
|
217
|
+
# and contributes no row.
|
|
218
|
+
#
|
|
219
|
+
# @param path — project directory; relative paths expand against the working directory
|
|
220
|
+
# @return a Hash from each absolute entry path, in `Dir.glob` order, to its kind; empty when not a directory
|
|
221
|
+
def list_directory_kinds(path)
|
|
222
|
+
absolute = File.absolute_path(path.to_s)
|
|
223
|
+
entries = RootedGlob.glob(absolute, "*").to_h { |entry| [entry, Cache::Descriptor::GlobEntry.lstat_kind(entry)] }
|
|
224
|
+
unless @policy.allow_read?(absolute)
|
|
225
|
+
record_refusal(absolute)
|
|
226
|
+
return entries
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
record_glob_entry(absolute, "*", mode: :kinds)
|
|
230
|
+
entries
|
|
231
|
+
end
|
|
232
|
+
|
|
202
233
|
# Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
|
|
203
234
|
# matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
|
|
204
235
|
# reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
|
|
@@ -209,8 +240,13 @@ module Rigor
|
|
|
209
240
|
#
|
|
210
241
|
# The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
|
|
211
242
|
# brace (`{..,x}`) or a backslash escape (`.\\./`) raises {AccessDeniedError} (`:read_outside_scope`).
|
|
212
|
-
# The last two are refused outright because `Dir.glob` can spell `..` with them.
|
|
213
|
-
#
|
|
243
|
+
# The last two are refused outright because `Dir.glob` can spell `..` with them. A leading `/` is read under
|
|
244
|
+
# `root`, as the joined spelling always read it (`/lib/**/*.rb` is `root/lib/**/*.rb`). Out of policy, `root`
|
|
245
|
+
# is globbed truthfully and contributes no row, as in {#list_directory}.
|
|
246
|
+
#
|
|
247
|
+
# `root` is read literally (#1959): {RootedGlob} escapes its glob metacharacters before the join, so a root
|
|
248
|
+
# spelled `fx[1]` or a directory named `{..,zz}` neither matches a sibling nor leaves itself.
|
|
249
|
+
# {#list_directory} lists the same way, and {Cache::Descriptor::GlobEntry} re-globs the row the same way.
|
|
214
250
|
#
|
|
215
251
|
# @param root — project directory; relative paths expand against the working directory
|
|
216
252
|
# @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
|
|
@@ -227,7 +263,7 @@ module Rigor
|
|
|
227
263
|
)
|
|
228
264
|
end
|
|
229
265
|
|
|
230
|
-
entries =
|
|
266
|
+
entries = RootedGlob.glob(absolute, pattern)
|
|
231
267
|
unless @policy.allow_read?(absolute)
|
|
232
268
|
record_refusal(absolute)
|
|
233
269
|
return entries
|
|
@@ -390,8 +426,8 @@ module Rigor
|
|
|
390
426
|
self.class.attribute(entry)
|
|
391
427
|
end
|
|
392
428
|
|
|
393
|
-
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}
|
|
394
|
-
# deduplicated per (root, pattern, mode) slot.
|
|
429
|
+
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob} and the
|
|
430
|
+
# `:kinds` row for {#list_directory_kinds}), deduplicated per (root, pattern, mode) slot.
|
|
395
431
|
# Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
|
|
396
432
|
# carries the signature of the state the LAST reader saw.
|
|
397
433
|
def record_glob_entry(root, pattern, mode: :stat)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "box"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Plugin
|
|
@@ -74,8 +75,8 @@ module Rigor
|
|
|
74
75
|
# requiring the gem, which is what the caller is about to do.
|
|
75
76
|
def bundle_require_paths(bundle_root)
|
|
76
77
|
bundle_gem_dirs(bundle_root).flat_map do |gem_home|
|
|
77
|
-
specs =
|
|
78
|
-
|
|
78
|
+
specs = RootedGlob.glob(gem_home, "specifications/*.gemspec")
|
|
79
|
+
.filter_map { |file| Gem::Specification.load(file) }
|
|
79
80
|
specs.group_by(&:name)
|
|
80
81
|
.flat_map { |_, versions| versions.max_by(&:version).full_require_paths }
|
|
81
82
|
end
|
|
@@ -88,7 +89,7 @@ module Rigor
|
|
|
88
89
|
return [] if bundle_root.nil? || bundle_root.to_s.empty?
|
|
89
90
|
|
|
90
91
|
root = bundle_root.to_s
|
|
91
|
-
(
|
|
92
|
+
(RootedGlob.glob(root, "ruby/*") + [root])
|
|
92
93
|
.select { |dir| File.directory?(File.join(dir, "specifications")) }
|
|
93
94
|
end
|
|
94
95
|
|
|
@@ -56,7 +56,7 @@ module Rigor
|
|
|
56
56
|
:hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
|
|
57
57
|
:source_rbs_synthesizer, :additional_initializers,
|
|
58
58
|
:effect_root, :effect_labels, :effect_attributions, :effect_edges, :effect_entry_points,
|
|
59
|
-
:effect_ancestry
|
|
59
|
+
:effect_ancestry, :per_invocation_classes, :per_invocation_non_action_macros
|
|
60
60
|
|
|
61
61
|
# #392 — the project-relative globs whose files this plugin compiles into {TemplateUnit}s. Purely a
|
|
62
62
|
# CLAIM: the engine globs these, reads each match's bytes and offers them to
|
|
@@ -74,7 +74,8 @@ module Rigor
|
|
|
74
74
|
hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
|
|
75
75
|
source_rbs_synthesizer: nil, additional_initializers: [],
|
|
76
76
|
effect_root: nil, effect_labels: [], effect_attributions: [], effect_edges: [],
|
|
77
|
-
effect_entry_points: [], effect_ancestry: [], template_globs: []
|
|
77
|
+
effect_entry_points: [], effect_ancestry: [], template_globs: [], per_invocation_classes: [],
|
|
78
|
+
per_invocation_non_action_macros: []
|
|
78
79
|
)
|
|
79
80
|
validate_id!(id)
|
|
80
81
|
validate_version!(version)
|
|
@@ -109,6 +110,7 @@ module Rigor
|
|
|
109
110
|
assign_additional_initializers(additional_initializers)
|
|
110
111
|
assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
|
|
111
112
|
effect_entry_points, effect_ancestry, template_globs)
|
|
113
|
+
assign_per_invocation_classes(per_invocation_classes, per_invocation_non_action_macros)
|
|
112
114
|
freeze
|
|
113
115
|
end
|
|
114
116
|
|
|
@@ -202,6 +204,28 @@ module Rigor
|
|
|
202
204
|
@template_globs = globs.map { |glob| glob.to_s.dup.freeze }.uniq.freeze
|
|
203
205
|
end
|
|
204
206
|
private :assign_template_globs
|
|
207
|
+
|
|
208
|
+
# ADR-125 — `per_invocation_classes:` names the framework classes whose every instance serves exactly one
|
|
209
|
+
# entry point, so ivar writes in two distinct entry points of a descendant are never live together.
|
|
210
|
+
# `per_invocation_non_action_macros:` names the class-body macros whose Symbol, String or inline-`def`
|
|
211
|
+
# arguments name a public method that is NOT an entry point (`before_action :load`, `helper_method
|
|
212
|
+
# :current_user`); they apply to this manifest's own `per_invocation_classes:`, so a list without classes
|
|
213
|
+
# is refused. Validated and assigned in one call, because `#initialize` is at its line budget.
|
|
214
|
+
def assign_per_invocation_classes(names, macros)
|
|
215
|
+
validate_array_of!("per_invocation_classes", names, "non-empty String") do |name|
|
|
216
|
+
name.is_a?(String) && !name.empty?
|
|
217
|
+
end
|
|
218
|
+
validate_array_of!("per_invocation_non_action_macros", macros, "Symbol/non-empty String") do |macro|
|
|
219
|
+
macro.is_a?(Symbol) || (macro.is_a?(String) && !macro.empty?)
|
|
220
|
+
end
|
|
221
|
+
if names.empty? && !macros.empty?
|
|
222
|
+
raise ArgumentError, "per_invocation_non_action_macros needs per_invocation_classes to apply to"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
@per_invocation_classes = names.map { |name| name.to_s.delete_prefix("::").dup.freeze }.uniq.freeze
|
|
226
|
+
@per_invocation_non_action_macros = macros.map(&:to_sym).uniq.freeze
|
|
227
|
+
end
|
|
228
|
+
private :assign_per_invocation_classes
|
|
205
229
|
# rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
|
|
206
230
|
|
|
207
231
|
public
|
|
@@ -258,7 +282,9 @@ module Rigor
|
|
|
258
282
|
"effect_edges" => effect_edges.map(&:to_h),
|
|
259
283
|
"effect_entry_points" => effect_entry_points.map(&:to_h),
|
|
260
284
|
"effect_ancestry" => effect_ancestry.map(&:to_h),
|
|
261
|
-
"template_globs" => template_globs
|
|
285
|
+
"template_globs" => template_globs,
|
|
286
|
+
"per_invocation_classes" => per_invocation_classes,
|
|
287
|
+
"per_invocation_non_action_macros" => per_invocation_non_action_macros.map(&:to_s)
|
|
262
288
|
}
|
|
263
289
|
end
|
|
264
290
|
|
|
@@ -397,6 +397,28 @@ module Rigor
|
|
|
397
397
|
@rbs_complete_extends_set.include?(class_name.to_s)
|
|
398
398
|
end
|
|
399
399
|
|
|
400
|
+
# ADR-125 — the aggregate class names declared across loaded plugins as per-invocation (manifest
|
|
401
|
+
# `per_invocation_classes:`): each instance of a descendant serves exactly one public-method entry point.
|
|
402
|
+
# `per_invocation_class?` is the exact-name membership predicate; `def.ivar-write-mismatch` walks a
|
|
403
|
+
# class's ancestry and asks it of each ancestor.
|
|
404
|
+
attr_reader :per_invocation_classes
|
|
405
|
+
|
|
406
|
+
def per_invocation_class?(class_name)
|
|
407
|
+
return false if class_name.nil?
|
|
408
|
+
|
|
409
|
+
@per_invocation_classes_set.include?(class_name.to_s.delete_prefix("::"))
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# ADR-125 — the macros whose arguments name a non-entry-point method on a descendant of the declared
|
|
413
|
+
# per-invocation class `class_name`: the union of `per_invocation_non_action_macros:` over the manifests
|
|
414
|
+
# that declare that class. An empty Set for a class no manifest declares.
|
|
415
|
+
def per_invocation_non_action_macros(class_name)
|
|
416
|
+
@per_invocation_macros.fetch(class_name.to_s.delete_prefix("::"), EMPTY_MACROS)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
EMPTY_MACROS = Set.new.freeze
|
|
420
|
+
private_constant :EMPTY_MACROS
|
|
421
|
+
|
|
400
422
|
# ADR-28 — flat, ordered list of every loaded plugin's path-scoped method-protocol contracts, in
|
|
401
423
|
# plugin registration order. Read from each plugin's `#protocol_contracts` (which the manifest backs
|
|
402
424
|
# by default but a plugin MAY override to fold in per-project config). Consumed by
|
|
@@ -465,12 +487,28 @@ module Rigor
|
|
|
465
487
|
@rbs_complete_ancestors_set = @rbs_complete_ancestors.to_set.freeze
|
|
466
488
|
@rbs_complete_extends = aggregate_class_names(manifests, :rbs_complete_extends)
|
|
467
489
|
@rbs_complete_extends_set = @rbs_complete_extends.to_set.freeze
|
|
490
|
+
@per_invocation_classes = aggregate_class_names(manifests, :per_invocation_classes)
|
|
491
|
+
@per_invocation_classes_set = @per_invocation_classes.to_set.freeze
|
|
492
|
+
@per_invocation_macros = compile_per_invocation_macros(manifests)
|
|
468
493
|
@type_node_resolvers = manifests.flat_map { |m| m&.type_node_resolvers || [] }.freeze
|
|
469
494
|
@protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
|
|
470
495
|
@contracts_by_path = {}
|
|
471
496
|
@effect_memo = {}
|
|
472
497
|
end
|
|
473
498
|
|
|
499
|
+
# ADR-125 — declared class name => frozen Set of the non-action macros its declaring manifests list.
|
|
500
|
+
def compile_per_invocation_macros(manifests)
|
|
501
|
+
table = Hash.new { |hash, key| hash[key] = Set.new }
|
|
502
|
+
manifests.each do |manifest|
|
|
503
|
+
next if manifest.nil?
|
|
504
|
+
|
|
505
|
+
manifest.per_invocation_classes.each do |name|
|
|
506
|
+
table[name].merge(manifest.per_invocation_non_action_macros)
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
table.to_h { |name, macros| [name, macros.freeze] }.freeze
|
|
510
|
+
end
|
|
511
|
+
|
|
474
512
|
# The shared shape of `open_receivers:` / `rbs_complete_ancestors:` — a flat uniq'd class-name list
|
|
475
513
|
# aggregated across manifests, each entry normalised to String.
|
|
476
514
|
def aggregate_class_names(manifests, field)
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../inference/parameter_inference_collector"
|
|
4
4
|
require_relative "../inference/scope_indexer"
|
|
5
|
+
require_relative "../inference/project_seed_tables"
|
|
6
|
+
require_relative "../cache/incremental_snapshot"
|
|
7
|
+
require_relative "../analysis/path_expansion"
|
|
8
|
+
require_relative "../analysis/buffer_binding"
|
|
5
9
|
|
|
6
10
|
module Rigor
|
|
7
11
|
module Protection
|
|
@@ -72,55 +76,79 @@ module Rigor
|
|
|
72
76
|
# @param paths — the measured file set, in the same order {#bundles} was built from.
|
|
73
77
|
# @param bundles — that bundle set.
|
|
74
78
|
# @param buffer — the mutant binding (logical path → mutant bytes).
|
|
79
|
+
# @param constant_sources — ADR-124 — also carry the per-name writer paths (`constant_sources`), which a
|
|
80
|
+
# `prebuilt:` runner needs to retract a `pre_eval:` constant another file also assigns (#663).
|
|
75
81
|
# @return frozen seed tables.
|
|
76
|
-
def tables_for_buffer(paths:, bundles:, buffer:)
|
|
82
|
+
def tables_for_buffer(paths:, bundles:, buffer:, constant_sources: false)
|
|
77
83
|
index = Inference::ScopeIndexer.discovered_project_index_incremental(
|
|
78
84
|
paths, seed_bundles: bundles, buffer: buffer
|
|
79
85
|
)
|
|
80
|
-
seed_tables(index)
|
|
86
|
+
tables = seed_tables(index)
|
|
87
|
+
sources = index.fetch(:def_index).fetch(:constant_sources)
|
|
88
|
+
tables = tables.merge(constant_sources: sources) if constant_sources && !sources.empty?
|
|
89
|
+
tables.freeze
|
|
81
90
|
end
|
|
82
91
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
92
|
+
# Issue #512 — the seed a one-shot probe (`rigor type-of`) can afford: the cross-file discovery tables for
|
|
93
|
+
# the configured paths, folded from the per-file bundles a prior `rigor check --incremental` left in the
|
|
94
|
+
# persistent snapshot, with `buffer`'s file (and any file whose bytes moved since) re-walked. A probe has
|
|
95
|
+
# no business re-parsing the project — that is what made the first attempt cost twice a single-file probe —
|
|
96
|
+
# so when there is no reusable snapshot this answers nil and the caller keeps its single-file scope. It
|
|
97
|
+
# never writes the snapshot.
|
|
86
98
|
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
# The snapshot is reused only under the fingerprint a bare `rigor check --incremental` writes (roots = the
|
|
100
|
+
# configured paths, the plain run mode): any other fingerprint, a different engine, configuration,
|
|
101
|
+
# lockfile or `sig/` included, is a miss.
|
|
102
|
+
#
|
|
103
|
+
# @param configuration — the probe's loaded configuration.
|
|
104
|
+
# @param buffer — the editor binding, or nil.
|
|
105
|
+
# @return frozen seed tables, or nil when the snapshot cannot seed this project.
|
|
106
|
+
def from_snapshot(configuration:, buffer: nil)
|
|
107
|
+
paths = Analysis::PathExpansion.configured_files(configuration)
|
|
108
|
+
return nil if paths.empty?
|
|
109
|
+
|
|
110
|
+
bundles = snapshot_bundles(configuration)
|
|
111
|
+
return nil if bundles.nil? || bundles.empty?
|
|
112
|
+
|
|
113
|
+
tables_for_buffer(paths: respell_for_buffer(paths, buffer), bundles: bundles, buffer: buffer,
|
|
114
|
+
constant_sources: true)
|
|
93
115
|
end
|
|
94
116
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
].each do |slot|
|
|
107
|
-
tables[seed_key(slot)] = def_index.fetch(slot)
|
|
117
|
+
# The configured file list with the entry that reaches `buffer`'s file respelled as the buffer's logical
|
|
118
|
+
# path: the binding substitutes bytes only for a path spelled exactly as its logical path, while the
|
|
119
|
+
# configured paths may be relative or reach the file through a symlink. `real_paths` is the memoised
|
|
120
|
+
# `BufferBinding.real_path` of each entry when the caller keeps one.
|
|
121
|
+
def respell_for_buffer(paths, buffer, real_paths: nil)
|
|
122
|
+
return paths if buffer.nil?
|
|
123
|
+
|
|
124
|
+
real = Analysis::BufferBinding.real_path(buffer.logical_path)
|
|
125
|
+
paths.each_with_index.map do |path, index|
|
|
126
|
+
entry_real = real_paths ? real_paths[index] : Analysis::BufferBinding.real_path(path)
|
|
127
|
+
entry_real == real ? buffer.logical_path : path
|
|
108
128
|
end
|
|
109
|
-
# ADR-119 WD1 — the pairs travel as one: an unpaired empty table is dropped here, a pair only when both
|
|
110
|
-
# halves are empty (`compact_pairs`), so a non-empty sibling is never stranded by an empty member.
|
|
111
|
-
tables.merge!(def_index.fetch(:siblings))
|
|
112
|
-
Scope::DiscoveryIndex.compact_pairs(
|
|
113
|
-
tables.reject { |name, table| !Scope::DiscoveryIndex.paired?(name) && (table.nil? || table.empty?) }
|
|
114
|
-
)
|
|
115
129
|
end
|
|
116
130
|
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
def snapshot_bundles(configuration)
|
|
132
|
+
fingerprint = Cache::IncrementalSnapshot.fingerprint(configuration: configuration, roots: configuration.paths)
|
|
133
|
+
return nil if fingerprint.nil?
|
|
134
|
+
|
|
135
|
+
loaded = Cache::IncrementalSnapshot.new(root: configuration.cache_path).load_any(fingerprints: [fingerprint])
|
|
136
|
+
loaded&.last&.seed_bundles
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The whole-project discovery tables, from the single-walk combined pass (one parse per file) that
|
|
140
|
+
# {Analysis::Runner::ProjectPrePasses#discover} uses, mapped by the one list {Inference::ProjectSeedTables}
|
|
141
|
+
# keeps (issue #1614). Empty tables are dropped so an empty seed stays `{}` and every consumer's "seed nothing"
|
|
142
|
+
# branch keeps working.
|
|
143
|
+
def discovery_tables(paths)
|
|
144
|
+
seed_tables(Inference::ScopeIndexer.discovered_project_index_for_paths(paths))
|
|
145
|
+
end
|
|
119
146
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
# Maps a `{ classes:, def_index: }` discovery index onto the {Scope::DiscoveryIndex} slot names both consumers
|
|
148
|
+
# take. Shared by the whole-walk ({#discovery_tables}) and bundle-fold ({#tables_for_buffer}) producers so a
|
|
149
|
+
# mutant's seed and the parent's seed can never disagree about shape.
|
|
150
|
+
def seed_tables(index)
|
|
151
|
+
Inference::ProjectSeedTables.from_index(index)
|
|
124
152
|
end
|
|
125
153
|
end
|
|
126
154
|
end
|