rigortype 0.4.0 → 0.4.2
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 +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -882,7 +882,7 @@ module Rigor
|
|
|
882
882
|
|
|
883
883
|
before, separator, = file.rpartition("/lib/")
|
|
884
884
|
root = separator.empty? ? File.dirname(file) : before
|
|
885
|
-
relative.map { |rel| File.
|
|
885
|
+
relative.map { |rel| File.absolute_path(rel, root) }
|
|
886
886
|
end
|
|
887
887
|
|
|
888
888
|
# ADR-28 — the path-scoped method-protocol contracts this plugin contributes. Defaults to the
|
|
@@ -940,6 +940,17 @@ module Rigor
|
|
|
940
940
|
# changed. The producer id is auto-prefixed `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
|
|
941
941
|
# caches stay sandboxed from built-in producers.
|
|
942
942
|
#
|
|
943
|
+
# #1558 — whichever way the value arrives, the boundary ends up holding every row of the producer's
|
|
944
|
+
# dependency descriptor. A hit ran no block and read nothing, so the served entry's stored rows are
|
|
945
|
+
# replayed into it ({IoBoundary#replay}); a miss read its inputs itself, and replaying the descriptor it
|
|
946
|
+
# just recorded adds the evaluated `watch:` rows, the only rows the block did not record. The boundary
|
|
947
|
+
# is per plugin and accumulates for the run, so the rows reach the ADR-45 run-result descriptor
|
|
948
|
+
# (`Runner#build_run_dependency_descriptor`) and the descriptor of every producer of this plugin
|
|
949
|
+
# computed after it, whether its block asked this one or it composes a value this one returned
|
|
950
|
+
# earlier. Without the replay a producer recomputed after a served one stored no row for the served
|
|
951
|
+
# value's inputs, and neither did the run: rigor-activerecord's `:model_index`, recomputed after a
|
|
952
|
+
# model edit while `:schema_table` hit, kept its stale columns after `db/schema.rb` changed.
|
|
953
|
+
#
|
|
943
954
|
# When `services.cache_store` is `nil` (e.g. CLI `--no-cache`), the callable bypasses the cache and
|
|
944
955
|
# runs the producer block every time — same semantics as the v0.0.9 cache surface for built-in
|
|
945
956
|
# producers.
|
|
@@ -974,11 +985,9 @@ module Rigor
|
|
|
974
985
|
generation_cap: producer[:generation_cap],
|
|
975
986
|
params: params,
|
|
976
987
|
serialize: pair_serializer(producer[:serialize]),
|
|
977
|
-
deserialize: pair_deserializer(producer[:deserialize])
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
[value, producer_dependency_descriptor(producer)]
|
|
981
|
-
end
|
|
988
|
+
deserialize: pair_deserializer(producer[:deserialize]),
|
|
989
|
+
on_hit: ->(dependencies) { io_boundary.replay(dependencies) }
|
|
990
|
+
) { computed_producer_pair(producer, compute) }
|
|
982
991
|
end
|
|
983
992
|
end
|
|
984
993
|
|
|
@@ -1151,7 +1160,7 @@ module Rigor
|
|
|
1151
1160
|
|
|
1152
1161
|
def collect_glob_files(roots, patterns)
|
|
1153
1162
|
matched = roots.flat_map do |root|
|
|
1154
|
-
absolute = File.
|
|
1163
|
+
absolute = File.absolute_path(root.to_s)
|
|
1155
1164
|
next [] unless File.directory?(absolute)
|
|
1156
1165
|
|
|
1157
1166
|
patterns.flat_map { |pattern| Dir.glob(File.join(absolute, pattern.to_s)) }
|
|
@@ -1196,6 +1205,17 @@ module Rigor
|
|
|
1196
1205
|
Cache::Descriptor.compose(auto_built, extra)
|
|
1197
1206
|
end
|
|
1198
1207
|
|
|
1208
|
+
# #1558 — the miss half of {#cache_for}. The block's own reads are in the boundary already, so replaying
|
|
1209
|
+
# the descriptor it just recorded adds the evaluated `watch:` rows, as a hit's replay of the stored
|
|
1210
|
+
# descriptor does; a producer computing around this one then records one row set whichever way this
|
|
1211
|
+
# value arrived.
|
|
1212
|
+
def computed_producer_pair(producer, compute)
|
|
1213
|
+
value = compute.call
|
|
1214
|
+
dependencies = producer_dependency_descriptor(producer)
|
|
1215
|
+
io_boundary.replay(dependencies)
|
|
1216
|
+
[value, dependencies]
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1199
1219
|
# ADR-60 WD3 — the dependency descriptor stored beside the producer's value, built AFTER the block ran
|
|
1200
1220
|
# so every in-block `io_boundary` read is captured, plus the evaluated `watch:` glob rows.
|
|
1201
1221
|
#
|
|
@@ -1224,7 +1244,7 @@ module Rigor
|
|
|
1224
1244
|
Array(tuples).flat_map do |tuple|
|
|
1225
1245
|
roots, *patterns = Array(tuple)
|
|
1226
1246
|
Array(roots).flat_map do |root|
|
|
1227
|
-
absolute = File.
|
|
1247
|
+
absolute = File.absolute_path(root.to_s)
|
|
1228
1248
|
patterns.map { |pattern| Cache::Descriptor::GlobEntry.compute(root: absolute, pattern: pattern.to_s) }
|
|
1229
1249
|
end
|
|
1230
1250
|
end.uniq
|
|
@@ -54,7 +54,7 @@ module Rigor
|
|
|
54
54
|
# by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
|
|
55
55
|
# where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
|
|
56
56
|
# a new plugin adopts the helper, add its manifest id here.
|
|
57
|
-
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
|
|
57
|
+
CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord alba factorybot rails-routes].freeze
|
|
58
58
|
|
|
59
59
|
# Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
|
|
60
60
|
# the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
|
|
@@ -43,10 +43,39 @@ module Rigor
|
|
|
43
43
|
# - `#cache_descriptor` — flushes the accumulated entries into a fresh {Cache::Descriptor} for the
|
|
44
44
|
# contribution that built it. URL fetches contribute `ConfigEntry` rows keyed `"url:#{url}"` with the
|
|
45
45
|
# response body's SHA-256 so contributions invalidate when the remote document changes.
|
|
46
|
+
# - `#replay(descriptor)` — records a {Cache::Descriptor}'s rows as though this boundary had observed
|
|
47
|
+
# them (#1558). A producer served from its own cache ran no block and read nothing, so
|
|
48
|
+
# {Plugin::Base#cache_for} replays the served entry's stored dependency rows here; the run-result
|
|
49
|
+
# descriptor and any enclosing producer's descriptor then carry the served value's inputs.
|
|
46
50
|
class IoBoundary
|
|
47
51
|
URL_TIMEOUT_SECONDS = 10
|
|
48
52
|
URL_MAX_BYTES = 10 * 1024 * 1024
|
|
49
53
|
|
|
54
|
+
ATTRIBUTION_KEY = :rigor_io_boundary_attribution
|
|
55
|
+
private_constant :ATTRIBUTION_KEY
|
|
56
|
+
|
|
57
|
+
# ADR-45 WD2 (#1507) — runs the block with `sink` (an Array) receiving every file and listing row ANY
|
|
58
|
+
# boundary records on this thread meanwhile, in addition to the boundary's own table, and returns the
|
|
59
|
+
# block's value. `Analysis::Runner` wraps each file's analysis in one when it records dependencies, so the
|
|
60
|
+
# `--incremental` session can keep the reads one file's analysis made beside that file's cached answer: a
|
|
61
|
+
# later run that serves the answer without re-analysing the file still has to validate what it read.
|
|
62
|
+
#
|
|
63
|
+
# A thread variable rather than a fiber-local, so a read a plugin makes from inside an `Enumerator` still
|
|
64
|
+
# lands in the sink. The boundary's own table is unchanged by it, including its first-existence-row-stands
|
|
65
|
+
# rule: the sink records what each read observed. Only reads are credited: a value a plugin memoised reaches
|
|
66
|
+
# later files without one, so the first file to trigger the read is the only one credited with it.
|
|
67
|
+
def self.attributing(sink)
|
|
68
|
+
previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
|
|
69
|
+
Thread.current.thread_variable_set(ATTRIBUTION_KEY, sink)
|
|
70
|
+
yield
|
|
71
|
+
ensure
|
|
72
|
+
Thread.current.thread_variable_set(ATTRIBUTION_KEY, previous)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.attribute(entry)
|
|
76
|
+
Thread.current.thread_variable_get(ATTRIBUTION_KEY)&.push(entry)
|
|
77
|
+
end
|
|
78
|
+
|
|
50
79
|
attr_reader :policy, :plugin_id
|
|
51
80
|
|
|
52
81
|
def initialize(policy:, plugin_id:, http_client: DefaultHttpClient.new)
|
|
@@ -61,7 +90,7 @@ module Rigor
|
|
|
61
90
|
@first_refused_path = nil
|
|
62
91
|
end
|
|
63
92
|
|
|
64
|
-
# Issue #959 — `TrustPolicy#allow_read?` refuses a path purely by `File.
|
|
93
|
+
# Issue #959 — `TrustPolicy#allow_read?` refuses a path purely by `File.absolute_path` comparison
|
|
65
94
|
# (ADR-2's documented bound; no `File.realpath`), so a project rooted under a symlink (macOS'
|
|
66
95
|
# `/tmp` → `/private/tmp` alias is the common case) can have every plugin read refused with no
|
|
67
96
|
# visible cause: the boundary answered "not readable" and the plugin swallowed or surfaced only its
|
|
@@ -96,7 +125,7 @@ module Rigor
|
|
|
96
125
|
# outcome is recorded: a path that exists but cannot be read (`EISDIR`, `EACCES`) is a failure the
|
|
97
126
|
# plugin reports, not a probe of absence, and records nothing — as before.
|
|
98
127
|
def read_file(path)
|
|
99
|
-
absolute = File.
|
|
128
|
+
absolute = File.absolute_path(path.to_s)
|
|
100
129
|
unless @policy.allow_read?(absolute)
|
|
101
130
|
record_refusal(absolute)
|
|
102
131
|
raise AccessDeniedError.new(
|
|
@@ -159,7 +188,7 @@ module Rigor
|
|
|
159
188
|
# @param path — project directory; relative paths expand against the working directory
|
|
160
189
|
# @return the absolute paths directly under `path` in `Dir.glob` order; empty when not a directory
|
|
161
190
|
def list_directory(path)
|
|
162
|
-
absolute = File.
|
|
191
|
+
absolute = File.absolute_path(path.to_s)
|
|
163
192
|
entries = Dir.glob(File.join(absolute, "*"))
|
|
164
193
|
unless @policy.allow_read?(absolute)
|
|
165
194
|
record_refusal(absolute)
|
|
@@ -170,6 +199,44 @@ module Rigor
|
|
|
170
199
|
entries
|
|
171
200
|
end
|
|
172
201
|
|
|
202
|
+
# Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
|
|
203
|
+
# matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
|
|
204
|
+
# reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
|
|
205
|
+
# when a match's bytes or stat tuple move. A caller whose answer depends on a match's content MUST
|
|
206
|
+
# {#read_file} each match it reads; that row carries the edit. A stat signature here would read stale on
|
|
207
|
+
# every `touch`, checkout or restored CI cache, though the content the caller read is unchanged (#979's
|
|
208
|
+
# reasoning). {#list_directory} keeps its `:stat` listing row.
|
|
209
|
+
#
|
|
210
|
+
# The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
|
|
211
|
+
# 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. Out of policy, `root` is
|
|
213
|
+
# globbed truthfully and contributes no row, as in {#list_directory}.
|
|
214
|
+
#
|
|
215
|
+
# @param root — project directory; relative paths expand against the working directory
|
|
216
|
+
# @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
|
|
217
|
+
# @return the absolute matching paths in `Dir.glob` order
|
|
218
|
+
def glob(root, pattern)
|
|
219
|
+
absolute = File.absolute_path(root.to_s)
|
|
220
|
+
pattern = pattern.to_s
|
|
221
|
+
if pattern.match?(/[{\\]/) || pattern.split("/").include?("..")
|
|
222
|
+
raise AccessDeniedError.new(
|
|
223
|
+
"plugin #{@plugin_id.inspect} cannot glob #{pattern.inspect} under #{absolute.inspect}: " \
|
|
224
|
+
"a `..` segment, a brace or a backslash can leave the root the trusted-read scope was checked against",
|
|
225
|
+
reason: :read_outside_scope,
|
|
226
|
+
resource: File.join(absolute, pattern)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
entries = Dir.glob(File.join(absolute, pattern))
|
|
231
|
+
unless @policy.allow_read?(absolute)
|
|
232
|
+
record_refusal(absolute)
|
|
233
|
+
return entries
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
record_glob_entry(absolute, pattern, mode: :names)
|
|
237
|
+
entries
|
|
238
|
+
end
|
|
239
|
+
|
|
173
240
|
# Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
|
|
174
241
|
# the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
|
|
175
242
|
# response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
|
|
@@ -202,8 +269,53 @@ module Rigor
|
|
|
202
269
|
Cache::Descriptor.new(files: files, configs: configs, globs: globs)
|
|
203
270
|
end
|
|
204
271
|
|
|
272
|
+
# #1558 — records every row of `descriptor` as though this boundary had observed it. The caller is
|
|
273
|
+
# {Plugin::Base#cache_for}: a producer served from its own cache ran no block, so without the replay
|
|
274
|
+
# the value's inputs reached neither the ADR-45 run-result descriptor nor the dependency descriptor of
|
|
275
|
+
# a producer computing around it, and both went on validating fresh after those inputs changed.
|
|
276
|
+
#
|
|
277
|
+
# The rows are the ones a fresh hit just validated, so they describe the current filesystem, and they
|
|
278
|
+
# are merged by the precedence the live recorders use. A content row (`:stat` / `:digest` / `:mtime`)
|
|
279
|
+
# replaces an existence row for its path, as {#read_file} does after a probe. Any other collision
|
|
280
|
+
# keeps the row already here: an existence row never displaces one, as in {#probe}, and between two
|
|
281
|
+
# content rows or two glob rows for one slot the live row carries the newer stat tuple, which keeps
|
|
282
|
+
# the next warm run on its stat fast path. The tables are keyed by path, glob slot and URL key, so
|
|
283
|
+
# replaying the same descriptor twice adds nothing. Row order reaches no cache key, because
|
|
284
|
+
# {Cache::Descriptor#to_canonical_hash} sorts every slot.
|
|
285
|
+
#
|
|
286
|
+
# The policy is not consulted: the rows were recorded under it when the entry was written, and replay
|
|
287
|
+
# reads no file. A {Cache::Descriptor::GlobEntry} keeps its `mode`, which is part of its slot key.
|
|
288
|
+
#
|
|
289
|
+
# ADR-45 WD2 (#1507) — every replayed row is also handed to the {.attributing} sink, the ones a held row
|
|
290
|
+
# wins over included, exactly as the live recorders hand over what each read observed: a producer first
|
|
291
|
+
# asked while a file is analysed (rigor-actionpack's `:controller_index`, rigor-rails-i18n's
|
|
292
|
+
# `:locale_index`) serves that file its inputs, and the `--incremental` slot must credit them to it. On a
|
|
293
|
+
# miss the replayed descriptor is the whole boundary's ({Plugin::Base#producer_dependency_descriptor}), so
|
|
294
|
+
# the file is also credited with rows other reads recorded; that over-credits, which only ever makes the
|
|
295
|
+
# slot decline sooner.
|
|
296
|
+
#
|
|
297
|
+
# @param descriptor — a dependency descriptor; only its `files`, `configs` and `globs` slots are read,
|
|
298
|
+
# the only slots a boundary records
|
|
299
|
+
def replay(descriptor)
|
|
300
|
+
@mutex.synchronize do
|
|
301
|
+
descriptor.files.each { |entry| replay_file_entry(entry) }
|
|
302
|
+
descriptor.configs.each { |entry| @config_entries[entry.key] ||= entry }
|
|
303
|
+
descriptor.globs.each { |entry| @glob_entries[entry.slot_key] ||= entry }
|
|
304
|
+
end
|
|
305
|
+
(descriptor.files + descriptor.globs).each { |entry| self.class.attribute(entry) }
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
205
309
|
private
|
|
206
310
|
|
|
311
|
+
# The {#replay} precedence for one file row; the caller holds `@mutex`.
|
|
312
|
+
def replay_file_entry(entry)
|
|
313
|
+
current = @file_entries[entry.path]
|
|
314
|
+
return if current && !(current.comparator == :exists && entry.comparator != :exists)
|
|
315
|
+
|
|
316
|
+
@file_entries[entry.path] = entry
|
|
317
|
+
end
|
|
318
|
+
|
|
207
319
|
# ADR-45 WD1b (#613) — the shared body of {#file?} / {#directory?}.
|
|
208
320
|
#
|
|
209
321
|
# The probe answers TRUTHFULLY for every path, in or out of the trusted-read scope, and never raises:
|
|
@@ -222,7 +334,7 @@ module Rigor
|
|
|
222
334
|
# reverse) → NOTHING, the same bound `#read_file` pins for `EISDIR`: the probe observed neither a
|
|
223
335
|
# usable file nor an absence, and either row would misdescribe what the plugin saw.
|
|
224
336
|
def probe(path)
|
|
225
|
-
absolute = File.
|
|
337
|
+
absolute = File.absolute_path(path.to_s)
|
|
226
338
|
answer = yield(absolute)
|
|
227
339
|
unless @policy.allow_read?(absolute)
|
|
228
340
|
record_refusal(absolute)
|
|
@@ -247,6 +359,7 @@ module Rigor
|
|
|
247
359
|
# A content row replaces an earlier existence row for the same path outright: the file appeared and
|
|
248
360
|
# its bytes were consumed, and a content row validates existence as well as content.
|
|
249
361
|
@mutex.synchronize { @file_entries[path] = entry }
|
|
362
|
+
self.class.attribute(entry)
|
|
250
363
|
end
|
|
251
364
|
|
|
252
365
|
# ADR-45 WD1 (#577) — the absence row for a probed-but-missing path. A content row already recorded
|
|
@@ -263,6 +376,7 @@ module Rigor
|
|
|
263
376
|
def record_absence_entry(path)
|
|
264
377
|
entry = Cache::Descriptor::FileEntry.absent(path: path)
|
|
265
378
|
@mutex.synchronize { @file_entries[path] ||= entry }
|
|
379
|
+
self.class.attribute(entry)
|
|
266
380
|
end
|
|
267
381
|
|
|
268
382
|
# ADR-45 WD1b (#613) — the presence row: fresh while the probed path exists, stale once it is gone.
|
|
@@ -273,14 +387,17 @@ module Rigor
|
|
|
273
387
|
def record_presence_entry(path)
|
|
274
388
|
entry = Cache::Descriptor::FileEntry.present(path: path)
|
|
275
389
|
@mutex.synchronize { @file_entries[path] ||= entry }
|
|
390
|
+
self.class.attribute(entry)
|
|
276
391
|
end
|
|
277
392
|
|
|
278
|
-
# ADR-45 WD1c (#629) — the listing row for {#list_directory}
|
|
393
|
+
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}),
|
|
394
|
+
# deduplicated per (root, pattern, mode) slot.
|
|
279
395
|
# Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
|
|
280
396
|
# carries the signature of the state the LAST reader saw.
|
|
281
|
-
def record_glob_entry(root, pattern)
|
|
282
|
-
entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
|
|
397
|
+
def record_glob_entry(root, pattern, mode: :stat)
|
|
398
|
+
entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern, mode: mode)
|
|
283
399
|
@mutex.synchronize { @glob_entries[entry.slot_key] = entry }
|
|
400
|
+
self.class.attribute(entry)
|
|
284
401
|
end
|
|
285
402
|
|
|
286
403
|
# Issue #959 — records that `path` fell outside every {TrustPolicy} read root, for {#refusal_summary}.
|
|
@@ -47,7 +47,7 @@ module Rigor
|
|
|
47
47
|
# build resolves the same root and skips the write — #414), and reading an unshareable value out of
|
|
48
48
|
# a module ivar from a non-main Ractor is the same `Ractor::IsolationError` as writing one.
|
|
49
49
|
def target_bundle_root=(root)
|
|
50
|
-
@target_bundle_root = root.nil? ? nil : File.
|
|
50
|
+
@target_bundle_root = root.nil? ? nil : File.absolute_path(root.to_s).freeze
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Requires `feature`, falling back to the analyzed project's bundler install tree. The fallback
|
|
@@ -42,8 +42,20 @@ module Rigor
|
|
|
42
42
|
# `Nominal[Grape::Endpoint]` (the block is `instance_eval`'d on an instance of that class, e.g. a
|
|
43
43
|
# route body on `Grape::Endpoint`), and `"singleton(Grape::API::Instance)"` binds
|
|
44
44
|
# `Singleton[Grape::API::Instance]` (the block is `instance_eval`'d on that *class object*, e.g.
|
|
45
|
-
# a `namespace` body on `Grape::API::Instance`).
|
|
46
|
-
#
|
|
45
|
+
# a `namespace` body on `Grape::API::Instance`). `:lexical` (issue #1667) leaves `self` as the caller's:
|
|
46
|
+
# the method runs the block where it was written (`block.refined(M).call`), so the entry only declares
|
|
47
|
+
# `refinements:`. Reserved Symbol names (`:receiver_singleton`, `:dsl_recorder`) remain unaccepted.
|
|
48
|
+
# - `refinements` — Array of fully-qualified module-name Strings (default `[]`, issue #1667, ADR-121 WD5).
|
|
49
|
+
# The method runs the block under `Proc#refined` with these modules (`instance_exec(&block.refined(M))`),
|
|
50
|
+
# so they are appended to the block body's in-effect refinements after its lexical list, in the declared
|
|
51
|
+
# order (the last one wins). Nested blocks inherit them. A module that refines nothing the project or a
|
|
52
|
+
# gem's source inference makes visible contributes nothing.
|
|
53
|
+
#
|
|
54
|
+
# ## Matching
|
|
55
|
+
#
|
|
56
|
+
# A `:receiver_instance` or `singleton(...)` entry matches `Singleton[X]` receivers only (a class-level DSL
|
|
57
|
+
# call). A named instance binding (`"Foo::Bar"`) and a `:lexical` entry also match `Nominal[X]` receivers:
|
|
58
|
+
# a `:lexical` method is as often an instance method (`ActiveRecord::Relation#where`) as a class-level one.
|
|
47
59
|
#
|
|
48
60
|
# ## Ractor-shareability
|
|
49
61
|
#
|
|
@@ -51,7 +63,8 @@ module Rigor
|
|
|
51
63
|
# mutable array does not leak into the value. `Ractor.shareable?` returns true after `#initialize`.
|
|
52
64
|
class BlockAsMethod
|
|
53
65
|
SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
|
|
54
|
-
|
|
66
|
+
SELF_TYPE_LEXICAL = :lexical
|
|
67
|
+
VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE, SELF_TYPE_LEXICAL].freeze
|
|
55
68
|
|
|
56
69
|
CLASS_NAME_PATTERN = /[A-Z]\w*(?:::[A-Z]\w*)*/
|
|
57
70
|
# `self_type: "Foo::Bar"` — the block is `instance_eval`'d on an instance of `Foo::Bar`.
|
|
@@ -60,16 +73,18 @@ module Rigor
|
|
|
60
73
|
# object itself (Grape's `namespace`/`route_param` bodies run on `Grape::API::Instance`).
|
|
61
74
|
SINGLETON_SELF_TYPE_PATTERN = /\Asingleton\((#{CLASS_NAME_PATTERN})\)\z/
|
|
62
75
|
|
|
63
|
-
attr_reader :receiver_constraint, :method_names, :self_type
|
|
76
|
+
attr_reader :receiver_constraint, :method_names, :self_type, :refinements
|
|
64
77
|
|
|
65
|
-
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
|
|
78
|
+
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE, refinements: [])
|
|
66
79
|
validate_receiver_constraint!(receiver_constraint)
|
|
67
80
|
validate_method_names!(method_names)
|
|
68
81
|
validate_self_type!(self_type)
|
|
82
|
+
validate_refinements!(refinements)
|
|
69
83
|
|
|
70
84
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
71
85
|
@method_names = method_names.map(&:to_sym).freeze
|
|
72
86
|
@self_type = self_type.is_a?(String) ? self_type.dup.freeze : self_type
|
|
87
|
+
@refinements = refinements.map { |name| name.dup.freeze }.freeze
|
|
73
88
|
freeze
|
|
74
89
|
end
|
|
75
90
|
|
|
@@ -77,7 +92,8 @@ module Rigor
|
|
|
77
92
|
{
|
|
78
93
|
"receiver_constraint" => receiver_constraint,
|
|
79
94
|
"method_names" => method_names.map(&:to_s),
|
|
80
|
-
"self_type" => self_type.to_s
|
|
95
|
+
"self_type" => self_type.to_s,
|
|
96
|
+
"refinements" => refinements
|
|
81
97
|
}
|
|
82
98
|
end
|
|
83
99
|
|
|
@@ -85,12 +101,18 @@ module Rigor
|
|
|
85
101
|
other.is_a?(BlockAsMethod) &&
|
|
86
102
|
receiver_constraint == other.receiver_constraint &&
|
|
87
103
|
method_names == other.method_names &&
|
|
88
|
-
self_type == other.self_type
|
|
104
|
+
self_type == other.self_type &&
|
|
105
|
+
refinements == other.refinements
|
|
89
106
|
end
|
|
90
107
|
alias eql? ==
|
|
91
108
|
|
|
92
109
|
def hash
|
|
93
|
-
[receiver_constraint, method_names, self_type].hash
|
|
110
|
+
[receiver_constraint, method_names, self_type, refinements].hash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Whether the block keeps the caller's `self` (`self_type: :lexical`).
|
|
114
|
+
def lexical_self?
|
|
115
|
+
self_type == SELF_TYPE_LEXICAL
|
|
94
116
|
end
|
|
95
117
|
|
|
96
118
|
# The class name a String `self_type` binds `self` to — `"Foo::Bar"` for both the plain and the
|
|
@@ -118,6 +140,12 @@ module Rigor
|
|
|
118
140
|
self_type.is_a?(String) && !singleton_binding?
|
|
119
141
|
end
|
|
120
142
|
|
|
143
|
+
# Whether `Nominal[X]` receivers match, beside `Singleton[X]` ones: a named instance binding, or a
|
|
144
|
+
# `:lexical` entry.
|
|
145
|
+
def matches_instance_receivers?
|
|
146
|
+
named_instance_binding? || lexical_self?
|
|
147
|
+
end
|
|
148
|
+
|
|
121
149
|
private
|
|
122
150
|
|
|
123
151
|
def validate_receiver_constraint!(value)
|
|
@@ -143,6 +171,21 @@ module Rigor
|
|
|
143
171
|
end
|
|
144
172
|
end
|
|
145
173
|
|
|
174
|
+
def validate_refinements!(refinements)
|
|
175
|
+
unless refinements.is_a?(Array)
|
|
176
|
+
raise ArgumentError,
|
|
177
|
+
"Plugin::Macro::BlockAsMethod#refinements must be an Array, got #{refinements.inspect}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
refinements.each do |name|
|
|
181
|
+
next if name.is_a?(String) && name.match?(NAMED_SELF_TYPE_PATTERN)
|
|
182
|
+
|
|
183
|
+
raise ArgumentError,
|
|
184
|
+
"Plugin::Macro::BlockAsMethod#refinements entries must be module-name Strings ('Foo::Bar'), " \
|
|
185
|
+
"got #{name.inspect}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
146
189
|
def validate_self_type!(self_type)
|
|
147
190
|
return if VALID_SELF_TYPES.include?(self_type)
|
|
148
191
|
return if self_type.is_a?(String) &&
|
|
@@ -36,7 +36,7 @@ module Rigor
|
|
|
36
36
|
|
|
37
37
|
@trusted_gems = trusted_gems.map { |g| g.to_s.dup.freeze }.uniq.sort.freeze
|
|
38
38
|
@allowed_read_roots = allowed_read_roots
|
|
39
|
-
.map { |path| File.
|
|
39
|
+
.map { |path| File.absolute_path(path).freeze }
|
|
40
40
|
.uniq
|
|
41
41
|
.sort
|
|
42
42
|
.freeze
|
|
@@ -46,10 +46,10 @@ module Rigor
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# @return true when the absolute path falls inside any allowed read root. Symlinks are
|
|
49
|
-
# resolved through `File.
|
|
49
|
+
# resolved through `File.absolute_path` only (no `realpath`); plugins with adversarial intent are out of
|
|
50
50
|
# scope per ADR-2.
|
|
51
51
|
def allow_read?(path)
|
|
52
|
-
absolute = File.
|
|
52
|
+
absolute = File.absolute_path(path.to_s)
|
|
53
53
|
@allowed_read_roots.any? { |root| inside?(absolute, root) }
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -53,16 +53,37 @@ module Rigor
|
|
|
53
53
|
# ADR-93 auto-wired `rigor-rbs-inline` entry when the library is resolvable).
|
|
54
54
|
# @param source_files — the file(s) the command inspects. Threaded so each loaded plugin's
|
|
55
55
|
# `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
|
|
56
|
-
|
|
56
|
+
# @param feature_sources — path => source text the required-feature scan reads in place of that file (an
|
|
57
|
+
# editor buffer bound with `--tmp-file` / `--instead-of`).
|
|
58
|
+
def build(configuration:, source_files:, feature_sources: {})
|
|
59
|
+
features = required_features(configuration, source_files, sources: feature_sources)
|
|
57
60
|
Environment.for_project(
|
|
58
61
|
libraries: configuration.libraries,
|
|
59
62
|
signature_paths: configuration.signature_paths,
|
|
60
63
|
plugin_registry: load_plugin_registry(configuration),
|
|
61
64
|
source_files: source_files,
|
|
65
|
+
required_features: features,
|
|
62
66
|
**dependency_discovery_options(configuration)
|
|
63
67
|
)
|
|
64
68
|
rescue StandardError
|
|
65
|
-
bare(configuration)
|
|
69
|
+
bare(configuration, features)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Issue #1700 — the {Environment::RequiredFeatures} the environment gates on: the configured paths plus
|
|
73
|
+
# `extra_files` (the file a probe inspects, which may lie outside them), the same set `rigor check` scans
|
|
74
|
+
# whichever files it is given, so a position types as `rigor check` types it.
|
|
75
|
+
def required_features(configuration, extra_files = [], sources: {})
|
|
76
|
+
Environment::RequiredFeatures.for_configuration(configuration, extra_files, sources: sources)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The required-feature scan's view of an editor buffer (`--tmp-file` / `--instead-of`): its logical path
|
|
80
|
+
# read from its physical file. Empty without a buffer or when the file cannot be read.
|
|
81
|
+
def buffer_sources(buffer)
|
|
82
|
+
return {} if buffer.nil?
|
|
83
|
+
|
|
84
|
+
{ buffer.logical_path => File.binread(buffer.physical_path) }
|
|
85
|
+
rescue SystemCallError
|
|
86
|
+
{}
|
|
66
87
|
end
|
|
67
88
|
|
|
68
89
|
# The dependency-discovery axes `check` reads off the configuration, as a keyword hash. Extracted so the
|
|
@@ -116,22 +137,24 @@ module Rigor
|
|
|
116
137
|
|
|
117
138
|
# The first fail-soft floor: no plugin tier and no synthesized RBS, but still the project's own dependency
|
|
118
139
|
# sources — dropping those is what issue #821 was, so a plugin-loading failure must not cost them.
|
|
119
|
-
def bare(configuration)
|
|
140
|
+
def bare(configuration, features = required_features(configuration))
|
|
120
141
|
Environment.for_project(
|
|
121
142
|
libraries: configuration.libraries,
|
|
122
143
|
signature_paths: configuration.signature_paths,
|
|
144
|
+
required_features: features,
|
|
123
145
|
**dependency_discovery_options(configuration)
|
|
124
146
|
)
|
|
125
147
|
rescue StandardError
|
|
126
|
-
minimal(configuration)
|
|
148
|
+
minimal(configuration, features)
|
|
127
149
|
end
|
|
128
150
|
|
|
129
151
|
# The last floor: RBS core plus the project's own signature paths, which cannot fail on anything outside
|
|
130
152
|
# the project itself.
|
|
131
|
-
def minimal(configuration)
|
|
153
|
+
def minimal(configuration, features = [])
|
|
132
154
|
Environment.for_project(
|
|
133
155
|
libraries: configuration.libraries,
|
|
134
|
-
signature_paths: configuration.signature_paths
|
|
156
|
+
signature_paths: configuration.signature_paths,
|
|
157
|
+
required_features: features
|
|
135
158
|
)
|
|
136
159
|
end
|
|
137
160
|
end
|
|
@@ -17,8 +17,8 @@ module Rigor
|
|
|
17
17
|
#
|
|
18
18
|
# The answer (issue #260's recorded decision) is one table set, built once, threaded to both sides: the
|
|
19
19
|
# {Mutator} base scope and {DiagnosticOracle}'s runner seam. This module builds that table set — the FULL
|
|
20
|
-
# discovery bundle (`discovered_def_nodes`, `discovered_methods`, superclasses, includes, …), not just
|
|
21
|
-
# class-identity slice site selection needed, because resolving `find` on `singleton(Account)` is what makes
|
|
20
|
+
# discovery bundle (`discovered_def_nodes`, `discovered_methods`, superclasses, includes, extends, …), not just
|
|
21
|
+
# the class-identity slice site selection needed, because resolving `find` on `singleton(Account)` is what makes
|
|
22
22
|
# the site killable.
|
|
23
23
|
#
|
|
24
24
|
# The shape is a plain Hash of {Scope::DiscoveryIndex} slot names, which is exactly what both consumers take:
|
|
@@ -84,9 +84,10 @@ module Rigor
|
|
|
84
84
|
# collectors driven over the same tree) that {Analysis::Runner::ProjectPrePasses#discover} uses. Empty
|
|
85
85
|
# tables are dropped so an empty seed stays `{}` and every consumer's "seed nothing" branch keeps working.
|
|
86
86
|
#
|
|
87
|
-
# `discovered_class_sources` is
|
|
88
|
-
#
|
|
89
|
-
#
|
|
87
|
+
# `discovered_class_sources` is carried as the runner carries it on every run: Tier 2 never records edges,
|
|
88
|
+
# but an ancestor-order reader asks whether a class is declared in more than one file. The mixin tables
|
|
89
|
+
# (`discovered_includes`, `discovered_prepends`, `discovered_extends`) ride the same fold, so a singleton
|
|
90
|
+
# `extend` reads the same from a per-file scope under coverage and protection as it does under `check`.
|
|
90
91
|
def discovery_tables(paths)
|
|
91
92
|
seed_tables(Inference::ScopeIndexer.discovered_project_index_for_paths(paths))
|
|
92
93
|
end
|
|
@@ -99,19 +100,27 @@ module Rigor
|
|
|
99
100
|
tables = { discovered_classes: index.fetch(:classes) }
|
|
100
101
|
%i[
|
|
101
102
|
def_nodes def_nestings singleton_def_nodes def_sources singleton_def_sources superclasses includes
|
|
102
|
-
prepends
|
|
103
|
+
extends prepends unpositioned_mixins class_sources
|
|
103
104
|
method_visibilities methods parameter_envelopes data_member_layouts struct_member_layouts
|
|
104
105
|
deferred_ranges refinements global_write_census
|
|
105
106
|
].each do |slot|
|
|
106
107
|
tables[seed_key(slot)] = def_index.fetch(slot)
|
|
107
108
|
end
|
|
108
|
-
|
|
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
|
+
)
|
|
109
115
|
end
|
|
110
116
|
|
|
117
|
+
UNPREFIXED_SLOTS = %i[data_member_layouts struct_member_layouts unpositioned_mixins].freeze
|
|
118
|
+
private_constant :UNPREFIXED_SLOTS
|
|
119
|
+
|
|
111
120
|
# The def-index slot names match the {Scope::DiscoveryIndex} ones under a `discovered_` prefix, except the
|
|
112
|
-
# two member-layout tables, which carry the same name on both sides.
|
|
121
|
+
# two member-layout tables and the unpositioned-mixin table, which carry the same name on both sides.
|
|
113
122
|
def seed_key(slot)
|
|
114
|
-
|
|
123
|
+
UNPREFIXED_SLOTS.include?(slot) ? slot : :"discovered_#{slot}"
|
|
115
124
|
end
|
|
116
125
|
end
|
|
117
126
|
end
|
|
@@ -140,7 +140,7 @@ module Rigor
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def snapshot_root_candidates(roots, configured)
|
|
143
|
-
[roots, Array(roots).map { |path| File.
|
|
143
|
+
[roots, Array(roots).map { |path| File.absolute_path(path) },
|
|
144
144
|
configured, Array(configured).map { |path| relative_to_pwd(path) }]
|
|
145
145
|
.compact.reject(&:empty?).uniq
|
|
146
146
|
end
|
|
@@ -148,8 +148,8 @@ module Rigor
|
|
|
148
148
|
# `path` rendered relative to the working directory when it lies underneath it; unchanged otherwise.
|
|
149
149
|
# Shared by the root-candidate list and the per-file snapshot lookup, which face the same skew.
|
|
150
150
|
def relative_to_pwd(path)
|
|
151
|
-
absolute = File.
|
|
152
|
-
prefix = "#{File.
|
|
151
|
+
absolute = File.absolute_path(path)
|
|
152
|
+
prefix = "#{File.absolute_path(Dir.pwd)}#{File::SEPARATOR}"
|
|
153
153
|
absolute.start_with?(prefix) ? absolute.delete_prefix(prefix) : path.to_s
|
|
154
154
|
end
|
|
155
155
|
|
|
@@ -220,7 +220,7 @@ module Rigor
|
|
|
220
220
|
def patched_rows(patched)
|
|
221
221
|
patched.by_key.map do |(class_name, method_name, kind), entry|
|
|
222
222
|
[class_name.to_s, method_name.to_s, kind.to_s, entry.source_path.to_s, entry.source_line,
|
|
223
|
-
entry.return_type&.describe(:short).to_s]
|
|
223
|
+
entry.return_type&.describe(:short).to_s, entry.alias_of.to_s]
|
|
224
224
|
end.sort
|
|
225
225
|
end
|
|
226
226
|
|