rigortype 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +167 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -113,6 +113,13 @@ module Rigor
|
|
|
113
113
|
)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# Whether one entry selects a class — by its fully-qualified name for a `namespace:` entry, by any
|
|
117
|
+
# file that defines it for a `match:` one.
|
|
118
|
+
#
|
|
119
|
+
# Public since #391: `rigor sig-gen`'s emission asks the same question this module's own
|
|
120
|
+
# {.for_classes} asks, and asks it with a defining file it already has. It was private while
|
|
121
|
+
# {.for_classes} was the only caller; a second caller with the same inputs is not a reason to
|
|
122
|
+
# reimplement the predicate.
|
|
116
123
|
def selects?(entry, class_name, paths)
|
|
117
124
|
return namespace_match?(entry.namespace, class_name) if entry.namespace
|
|
118
125
|
|
|
@@ -164,7 +171,7 @@ module Rigor
|
|
|
164
171
|
def files_by_class(sources, project_root)
|
|
165
172
|
root = "#{File.absolute_path(project_root.to_s).chomp('/')}/"
|
|
166
173
|
sources.each_with_object({}) do |(key, paths), out|
|
|
167
|
-
owner = MethodKey.
|
|
174
|
+
owner = MethodKey.envelope_owner(key)
|
|
168
175
|
next if owner.nil?
|
|
169
176
|
|
|
170
177
|
bucket = (out[owner] ||= [])
|
|
@@ -177,7 +184,7 @@ module Rigor
|
|
|
177
184
|
absolute.start_with?(root) ? absolute[root.length..] : path.to_s
|
|
178
185
|
end
|
|
179
186
|
|
|
180
|
-
private_class_method :
|
|
187
|
+
private_class_method :match_segments?, :match_deep?, :files_by_class, :relativize
|
|
181
188
|
end
|
|
182
189
|
end
|
|
183
190
|
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Effects
|
|
7
|
+
# Which side of its class a definition written at some point lands on, and what `self` its body runs
|
|
8
|
+
# on, or that the syntax does not say. The scanner keys a unit `Class.m` or `Class#m` from the first, and
|
|
9
|
+
# scans the body with the second (the singleton bit {MutationClassifier} and
|
|
10
|
+
# {LocalOwnership.constructor?} read).
|
|
11
|
+
#
|
|
12
|
+
# Ruby answers the two from different places, which is why one bit carried down the walk got both
|
|
13
|
+
# wrong in turn:
|
|
14
|
+
#
|
|
15
|
+
# - a receiver-less `def` lands on the **default definee**: the class the lexically enclosing body
|
|
16
|
+
# opened, which is the singleton class inside `class << self`. A method body does not move it, so
|
|
17
|
+
# `def inner` inside `def self.outer` defines `Foo#inner`, while inside a `def` in `class << self` it
|
|
18
|
+
# defines `Foo.inner`.
|
|
19
|
+
# - `define_method` and the `attr_*` macros are calls on **`self`**, so they define instance methods of
|
|
20
|
+
# the module `self` is: `Foo#x` inside `def self.setup`, where `self` is `Foo`, and `Foo.x` inside
|
|
21
|
+
# `class << self`, where it is `Foo`'s singleton class.
|
|
22
|
+
#
|
|
23
|
+
# Only the spellings that name the new `self` move either answer: `class << self`, and a block given to
|
|
24
|
+
# `class_eval` / `instance_eval` (or an `_exec` / `module_` alias) on `self` or on `singleton_class`.
|
|
25
|
+
# Where the syntax cannot say, the answer is **unknown** and a definition there is no unit. That covers
|
|
26
|
+
# the block of `Class.new` and its kin, an eval on any other receiver, `class << obj`, and a method
|
|
27
|
+
# defined on one object or on a singleton class's own singleton class. Filed under the enclosing class,
|
|
28
|
+
# such a definition would join that class's real method of the same name, and a `Class.new(self)`
|
|
29
|
+
# override shares its name by design.
|
|
30
|
+
#
|
|
31
|
+
# `self_kind` is `:instance`, `:class` (the class the enclosing namespace opened), `:singleton_class`
|
|
32
|
+
# (that class's singleton class) or `:unknown`, and `definee_singleton` is true, false, or nil when
|
|
33
|
+
# unknown. Every context is allocated once.
|
|
34
|
+
class DefinitionContext < Data.define(:self_kind, :definee_singleton)
|
|
35
|
+
CLASS_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec].to_set.freeze
|
|
36
|
+
INSTANCE_EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
|
|
37
|
+
# The builders whose block defines into a class or module the call itself creates.
|
|
38
|
+
ANONYMOUS_BUILDERS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
|
|
39
|
+
# The `self_kind`s that are a class object.
|
|
40
|
+
CLASS_SELVES = %i[class singleton_class].to_set.freeze
|
|
41
|
+
private_constant :CLASS_EVAL_CALLS, :INSTANCE_EVAL_CALLS, :ANONYMOUS_BUILDERS, :CLASS_SELVES
|
|
42
|
+
|
|
43
|
+
CONTEXTS = %i[instance class singleton_class unknown].to_h do |self_kind|
|
|
44
|
+
[self_kind, [false, true, nil].to_h do |definee|
|
|
45
|
+
[definee, new(self_kind: self_kind, definee_singleton: definee)]
|
|
46
|
+
end.freeze]
|
|
47
|
+
end.freeze
|
|
48
|
+
private_constant :CONTEXTS
|
|
49
|
+
|
|
50
|
+
def self.of(self_kind, definee_singleton)
|
|
51
|
+
CONTEXTS.fetch(self_kind).fetch(definee_singleton)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Whether a child of `node` may run under another context than `node` does: a call's block, or a
|
|
55
|
+
# `class <<` body. Only these need {#for_child}, and asking once per node rather than once per child
|
|
56
|
+
# keeps the walks from paying a call for every element of a large literal.
|
|
57
|
+
def self.rebinds?(node)
|
|
58
|
+
node.is_a?(Prism::SingletonClassNode) || (node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A class body.
|
|
62
|
+
CLASS_BODY = of(:class, false)
|
|
63
|
+
# A `class << self` body: `self` and the definee are both the singleton class.
|
|
64
|
+
SINGLETON_CLASS_BODY = of(:singleton_class, true)
|
|
65
|
+
# The body of an instance method of the class.
|
|
66
|
+
INSTANCE_METHOD_BODY = of(:instance, false)
|
|
67
|
+
# The top level, where `self` is `main`, an instance of `Object`, and a `def` defines `Object#m`.
|
|
68
|
+
TOP_LEVEL = INSTANCE_METHOD_BODY
|
|
69
|
+
# Where the syntax says neither what `self` is nor where a `def` lands. Every block inside stays here.
|
|
70
|
+
UNKNOWN = of(:unknown, nil)
|
|
71
|
+
|
|
72
|
+
# Whether `self` is a class object here — the singleton bit a unit's body is scanned with.
|
|
73
|
+
def singleton?
|
|
74
|
+
CLASS_SELVES.include?(self_kind)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether `self` is the singleton class here, so a call on it (`define_method`, an `attr_*` macro,
|
|
78
|
+
# `include`, `prepend`, `alias_method`) works on the singleton class rather than the class.
|
|
79
|
+
def self_singleton_class?
|
|
80
|
+
self_kind == :singleton_class
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The context the body of `def` node `node` runs under, or nil where no key names the method. The
|
|
84
|
+
# unit is keyed singleton exactly when that context is {#singleton?}. The body keeps this default
|
|
85
|
+
# definee: a `def` nested in it lands where one written beside it would.
|
|
86
|
+
#
|
|
87
|
+
# `def self.x` defines a singleton method only where `self` is the class. In an instance method it
|
|
88
|
+
# defines a method on one object, and in `class << self` one on the singleton class's own singleton
|
|
89
|
+
# class. `def Const.x` keeps the reading it has always had, a method of the class the unit is keyed
|
|
90
|
+
# under; any other receiver is an object no key names.
|
|
91
|
+
def def_body(node)
|
|
92
|
+
case node.receiver
|
|
93
|
+
when nil
|
|
94
|
+
DefinitionContext.of(definee_singleton ? :class : :instance, definee_singleton) unless definee_singleton.nil?
|
|
95
|
+
when Prism::SelfNode
|
|
96
|
+
self if self_kind == :class
|
|
97
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
98
|
+
DefinitionContext.of(:class, definee_singleton) unless self_kind == :unknown
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The context the block of a literal-name `define_method` written here runs under, or nil where no key
|
|
103
|
+
# names the method; an `attr_*` macro here defines on the same side. Both are calls on `self`, which
|
|
104
|
+
# only a class and its singleton class answer, and the block keeps the definee it closed over.
|
|
105
|
+
def module_call_body
|
|
106
|
+
case self_kind
|
|
107
|
+
when :class then DefinitionContext.of(:instance, definee_singleton)
|
|
108
|
+
when :singleton_class then DefinitionContext.of(:class, definee_singleton)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The context `child` runs under as a child of `node`, a node {.rebinds?} accepts. A call's block and a
|
|
113
|
+
# `class <<` body may rebind `self`; every other child keeps this context. (A block is a call's only
|
|
114
|
+
# `BlockNode` child.)
|
|
115
|
+
def for_child(node, child)
|
|
116
|
+
case node
|
|
117
|
+
when Prism::CallNode then child.is_a?(Prism::BlockNode) ? block(node) : self
|
|
118
|
+
when Prism::SingletonClassNode then child.equal?(node.body) ? singleton_class_body(node.expression) : self
|
|
119
|
+
else self
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# `class << self` on the class opens its singleton class; any other `class << …` opens one no key names.
|
|
126
|
+
def singleton_class_body(expression)
|
|
127
|
+
self_kind == :class && expression.is_a?(Prism::SelfNode) ? SINGLETON_CLASS_BODY : UNKNOWN
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def block(call)
|
|
131
|
+
name = call.name
|
|
132
|
+
if CLASS_EVAL_CALLS.include?(name) then class_eval_block(call.receiver)
|
|
133
|
+
elsif INSTANCE_EVAL_CALLS.include?(name) then instance_eval_block(call.receiver)
|
|
134
|
+
elsif anonymous_builder?(call) then UNKNOWN
|
|
135
|
+
else self
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# `class_eval` makes its receiver both `self` and the definee.
|
|
140
|
+
def class_eval_block(receiver)
|
|
141
|
+
if self_receiver?(receiver) && singleton? then DefinitionContext.of(self_kind, self_kind == :singleton_class)
|
|
142
|
+
elsif singleton_class_receiver?(receiver) then SINGLETON_CLASS_BODY
|
|
143
|
+
else UNKNOWN
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `instance_eval` makes its receiver `self` and the receiver's singleton class the definee. On the
|
|
148
|
+
# class that is the singleton class. On the singleton class it is that class's own singleton class,
|
|
149
|
+
# which no key names, so a `define_method` there is keyed and a `def` is not.
|
|
150
|
+
def instance_eval_block(receiver)
|
|
151
|
+
if self_receiver?(receiver) && self_kind == :class then DefinitionContext.of(:class, true)
|
|
152
|
+
elsif (self_receiver?(receiver) && self_kind == :singleton_class) || singleton_class_receiver?(receiver)
|
|
153
|
+
DefinitionContext.of(:singleton_class, nil)
|
|
154
|
+
else UNKNOWN
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self_receiver?(receiver)
|
|
159
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `singleton_class` called on the class.
|
|
163
|
+
def singleton_class_receiver?(receiver)
|
|
164
|
+
self_kind == :class && receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class &&
|
|
165
|
+
self_receiver?(receiver.receiver) && receiver.arguments.nil? && receiver.block.nil?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# `Class.new { … }` and its kin, spelled on the core constant.
|
|
169
|
+
def anonymous_builder?(call)
|
|
170
|
+
receiver = call.receiver
|
|
171
|
+
name = case receiver
|
|
172
|
+
when Prism::ConstantReadNode then receiver.name
|
|
173
|
+
when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
|
|
174
|
+
end
|
|
175
|
+
!name.nil? && ANONYMOUS_BUILDERS[name] == call.name
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -29,11 +29,19 @@ module Rigor
|
|
|
29
29
|
# The lanes are kept **raw** here — a declared label a proven one already subsumes is dropped where
|
|
30
30
|
# output is rendered ({LabelSet#excluding_subsumed_by}), never in the table, because a further join
|
|
31
31
|
# has to see what was actually declared.
|
|
32
|
+
# `unclaimed` is the transitive reading of {Summary#unclaimed?}: true when this method, or anything
|
|
33
|
+
# it reaches, called something nothing described (#391). It is not a label, not a taint, and is read
|
|
34
|
+
# by `rigor sig-gen`'s annotation emission alone — not by the report, the snapshot, or any
|
|
35
|
+
# diagnostic.
|
|
32
36
|
class Entry < Data.define(:key, :direct, :proven, :undischarged, :declared, :exhaustive, :causes,
|
|
33
|
-
:edges)
|
|
34
|
-
def initialize(undischarged: nil, declared: nil, **rest)
|
|
37
|
+
:edges, :unclaimed)
|
|
38
|
+
def initialize(undischarged: nil, declared: nil, unclaimed: false, **rest)
|
|
35
39
|
super(undischarged: undischarged || rest.fetch(:proven), declared: declared || LabelSet::EMPTY,
|
|
36
|
-
**rest)
|
|
40
|
+
unclaimed: unclaimed ? true : false, **rest)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unclaimed?
|
|
44
|
+
unclaimed
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
# The declared labels worth printing beside `proven` — the rendering rule, in one place so the
|
|
@@ -157,7 +157,7 @@ module Rigor
|
|
|
157
157
|
# synthesized `attr_*` / `define_method` members the effects scanner adds.
|
|
158
158
|
def keys_by_class(table)
|
|
159
159
|
table.keys.each_with_object({}) do |key, out|
|
|
160
|
-
owner = MethodKey.
|
|
160
|
+
owner = MethodKey.envelope_owner(key)
|
|
161
161
|
next if owner.nil?
|
|
162
162
|
|
|
163
163
|
(out[owner] ||= []) << key
|
|
@@ -135,6 +135,21 @@ module Rigor
|
|
|
135
135
|
envelope
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# Whether ANY stratum carries an annotation for this key — ⊤ envelopes included.
|
|
139
|
+
#
|
|
140
|
+
# {#[]} is the import question and rightly refuses a ⊤ envelope: a bound that bounds nothing is not
|
|
141
|
+
# a bound. `rigor sig-gen`'s emission asks the other one (#391): an author who wrote
|
|
142
|
+
# `%a{rigor:v1:effect bogus.nonsense}` has still written about this method, and replacing their
|
|
143
|
+
# typo with an inferred `%a{pure}` would delete the only evidence that they meant to bound it — the
|
|
144
|
+
# `effect.unknown-label` report points at the annotation, and there would no longer be one.
|
|
145
|
+
def annotated?(owner, singleton, selector)
|
|
146
|
+
return false if owner.nil? || empty?
|
|
147
|
+
|
|
148
|
+
key = "#{owner}#{singleton ? '.' : '#'}#{selector}"
|
|
149
|
+
!(@method_envelopes[key] || @class_envelopes[owner] || config_envelope(owner) ||
|
|
150
|
+
@accepted[key]).nil?
|
|
151
|
+
end
|
|
152
|
+
|
|
138
153
|
private
|
|
139
154
|
|
|
140
155
|
# The first `namespace:` entry selecting `owner`, memoised per class name — one call site's owner
|
|
@@ -35,12 +35,59 @@ module Rigor
|
|
|
35
35
|
# selector rather than a receiver's, and the propagator resolves it against the ancestry *above* that
|
|
36
36
|
# class with no closed-world override join — a different question from every other edge, which is why
|
|
37
37
|
# it is a field rather than a convention over the other three.
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# `constant_receiver` marks a call whose receiver the author wrote as a **constant path** (#1039).
|
|
40
|
+
# The collector keys an edge on the receiver's TYPE, so `self.class.new` inside `Base`, `klass.new`
|
|
41
|
+
# on a `Singleton[Base]` local and a receiver-less `new` in a singleton body all produce the very
|
|
42
|
+
# same tuple as a literal `Base.new` — and the first three really do construct a subclass. Only the
|
|
43
|
+
# constructor rule reads it, and only to DROP the closed-world subclass join a written constant
|
|
44
|
+
# cannot reach.
|
|
45
|
+
#
|
|
46
|
+
# `unclaimed` marks a site NOTHING bounded: no catalogue row, no plugin row, no imported envelope
|
|
47
|
+
# (#391). It is not a taint and never becomes one — an unresolved edge here is overwhelmingly an
|
|
48
|
+
# inherited or gem call the catalogue simply has no row for, which is why the model drops it — but
|
|
49
|
+
# it IS the difference between "the analyzer read every callee" and "the analyzer read the callees
|
|
50
|
+
# it had something to read". Only sig-gen's annotation emission consults it, through
|
|
51
|
+
# {EffectTable::Entry#unclaimed?}: writing `%a{pure}` is a claim about a callee nobody described,
|
|
52
|
+
# and the emitter must decline rather than invent one.
|
|
53
|
+
# `taint_if_unresolved` is #1048's inversion of a taint. A `render :show` whose {CalleeRule} named
|
|
54
|
+
# `view:users/show.html` may or may not reach a unit — only the merged table knows, exactly as only
|
|
55
|
+
# the merged ancestry knows whether a `super` resolves — so the row's `template-not-analysed` cause
|
|
56
|
+
# travels ON the edge as a frozen `[cause, detail]` pair and {Propagator} seeds it only where the
|
|
57
|
+
# edge landed on nothing. Adding a cause where the walk failed rather than subtracting one where it
|
|
58
|
+
# succeeded keeps every step of the fixpoint monotone, and it is the same shape
|
|
59
|
+
# {Propagator.taint_unresolved_super} already has.
|
|
60
|
+
#
|
|
61
|
+
# `fallback_selectors` is #1065's lookup order: the selectors the propagator retries, in order, when
|
|
62
|
+
# `selector` resolves to nothing — `["html"]` on a `view:watchers/_watchers` + `js` edge, because a
|
|
63
|
+
# `.js.erb` template's lookup context is `[:js, :html]`. The FIRST that resolves is the edge's
|
|
64
|
+
# target and the taint stays unseeded; only when every one fails is `taint_if_unresolved` seeded.
|
|
65
|
+
# One ordered list rather than one edge per candidate, because two edges would join BOTH units
|
|
66
|
+
# where both exist, and only one of them runs.
|
|
67
|
+
Edge = Data.define(:receiver_class, :kind, :selector, :self_call, :super_call, :unclaimed,
|
|
68
|
+
:constant_receiver, :taint_if_unresolved, :fallback_selectors) do
|
|
39
69
|
# Defaulted because every producer but the `super` one records an ordinary call, and an ordinary
|
|
40
|
-
# call is not a `super
|
|
41
|
-
|
|
70
|
+
# call is not a `super`, and because a producer that says nothing about `unclaimed` /
|
|
71
|
+
# `constant_receiver` means the safe value of each.
|
|
72
|
+
#
|
|
73
|
+
# The defaults do NOT rescue a cache written before a member existed: `Marshal.load` of a `Data`
|
|
74
|
+
# whose member list has grown raises `TypeError: struct size differs`, which the store reads as a
|
|
75
|
+
# miss. That is the right outcome and not the one relied on — the cache identity carries a schema
|
|
76
|
+
# component ({Identity}, `schema:5`), so such an entry is never offered in the first place.
|
|
77
|
+
def initialize(super_call: false, unclaimed: false, constant_receiver: false,
|
|
78
|
+
taint_if_unresolved: nil, fallback_selectors: nil, **)
|
|
79
|
+
super
|
|
80
|
+
end
|
|
42
81
|
end
|
|
43
82
|
|
|
83
|
+
# An ancestry entry that names nothing and can never resolve. The scanner records it where a class
|
|
84
|
+
# body says its ancestry or its constructor is not readable from the source — a superclass
|
|
85
|
+
# expression that is not a constant path (`class K < Struct.new(:a)`), or an `alias` / `alias_method`
|
|
86
|
+
# that makes `initialize` some other method (#1039). It matches no summary key, so every existing
|
|
87
|
+
# walk steps over it; the constructor rule asks for it by name, because "unreadable" is exactly what
|
|
88
|
+
# that rule must not read as "empty".
|
|
89
|
+
OPAQUE_ANCESTOR = "<opaque>"
|
|
90
|
+
|
|
44
91
|
NO_TABLE = {}.freeze
|
|
45
92
|
private_constant :NO_TABLE
|
|
46
93
|
|
|
@@ -107,7 +154,12 @@ module Rigor
|
|
|
107
154
|
|
|
108
155
|
fold_summaries(summaries, collection.summaries)
|
|
109
156
|
fold_lists(edges, collection.edges)
|
|
110
|
-
|
|
157
|
+
# The opaque sentinel is sticky across files as well as within one: a class whose constructor one
|
|
158
|
+
# file says is unreadable does not become readable because another file spells its `<`, and the
|
|
159
|
+
# fold must not depend on which file the run reads first.
|
|
160
|
+
superclasses.update(collection.superclasses) do |_key, mine, theirs|
|
|
161
|
+
mine.include?(OPAQUE_ANCESTOR) ? mine : theirs
|
|
162
|
+
end
|
|
111
163
|
fold_lists(includes, collection.includes)
|
|
112
164
|
end
|
|
113
165
|
|
|
@@ -167,7 +219,9 @@ module Rigor
|
|
|
167
219
|
end
|
|
168
220
|
|
|
169
221
|
def edge_order(edge)
|
|
170
|
-
[edge.receiver_class.to_s, edge.kind.to_s, edge.selector, edge.self_call ? 1 : 0,
|
|
222
|
+
[edge.receiver_class.to_s, edge.kind.to_s, edge.selector, edge.self_call ? 1 : 0,
|
|
223
|
+
edge.super_call ? 1 : 0, edge.constant_receiver ? 1 : 0,
|
|
224
|
+
Array(edge.taint_if_unresolved).join("\x00"), Array(edge.fallback_selectors).join("\x00")]
|
|
171
225
|
end
|
|
172
226
|
end
|
|
173
227
|
end
|
|
@@ -196,7 +196,7 @@ module Rigor
|
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
# The uniqueness validator's own query. It rides the DECLARED lane with no taint, exactly as every
|
|
199
|
-
# other first-party plugin contribution does (ADR-103 WD6): the plugin read the app's own
|
|
199
|
+
# other first-party plugin contribution does (ADR-103 WD6 / WD17): the plugin read the app's own
|
|
200
200
|
# `validates … uniqueness: true` and knows what Rails does with it, but the analyzer did not read a
|
|
201
201
|
# body, so this is a trusted claim rather than a proof.
|
|
202
202
|
def uniqueness_summary(class_name)
|
|
@@ -51,6 +51,22 @@ module Rigor
|
|
|
51
51
|
def digest(configuration:, registry: Registry.default, catalog: Catalog.default, plugin_facts: nil)
|
|
52
52
|
Digest::SHA256.hexdigest(
|
|
53
53
|
[
|
|
54
|
+
# #391 — the summary schema itself. A cached collection written before {Summary#unclaimed?}
|
|
55
|
+
# existed restores with the bit false, which is the direction sig-gen's emission must never
|
|
56
|
+
# guess, so such an entry must not be served rather than merely read carefully.
|
|
57
|
+
#
|
|
58
|
+
# schema:3 — #1039 moved edge RESOLUTION: a singleton `new` edge on a project class now
|
|
59
|
+
# reaches that class's `#initialize`. The warm lanes serve a propagated `EffectTable` (and its
|
|
60
|
+
# `unclaimed` bits) rather than re-resolving, so an entry written under the old rule would
|
|
61
|
+
# answer the old closure for source nothing changed.
|
|
62
|
+
#
|
|
63
|
+
# schema:4 — #1048 added {FileCollection::Edge#taint_if_unresolved}, so a marshalled edge
|
|
64
|
+
# written under schema:3 no longer restores at all (`Data` with a grown member list raises
|
|
65
|
+
# `TypeError`), and a warm entry would otherwise be offered and then read as a miss.
|
|
66
|
+
#
|
|
67
|
+
# schema:5 — #1065 added {FileCollection::Edge#fallback_selectors}, which both grows the
|
|
68
|
+
# marshalled member list again and changes which unit a cached `view:` edge resolves to.
|
|
69
|
+
"schema:5",
|
|
54
70
|
"vocabulary:#{registry.vocabulary_version}",
|
|
55
71
|
"catalog:#{catalog.identity}",
|
|
56
72
|
"effects:#{config_digest(configuration)}",
|
|
@@ -11,8 +11,8 @@ module Rigor
|
|
|
11
11
|
#
|
|
12
12
|
# Ruby has no by-ref parameters, so `mutate.local` cannot mean "a write into an out-parameter" as it
|
|
13
13
|
# does in PHP. It means the mutated receiver is invisible to the caller, and that is an ownership
|
|
14
|
-
# question: a local whose every assignment allocates (`[]`, `{}`, `""`,
|
|
15
|
-
# escapes the body is one no caller can observe being mutated.
|
|
14
|
+
# question: a local whose every assignment allocates (`[]`, `{}`, `""`, `Foo.new`, `.dup`) and which
|
|
15
|
+
# never escapes the body is one no caller can observe being mutated.
|
|
16
16
|
#
|
|
17
17
|
# The analysis is deliberately **flow-insensitive and whole-body**: a local that escapes anywhere
|
|
18
18
|
# disqualifies, even if the escape happens after the mutation. That is strictly more conservative than
|
|
@@ -23,16 +23,17 @@ module Rigor
|
|
|
23
23
|
# This is the tracer slice's approximation, not the eventual answer. `ClosureEscapeAnalyzer` answers a
|
|
24
24
|
# different question (fact retention, not "does the code contain") and is deliberately left alone.
|
|
25
25
|
module LocalOwnership
|
|
26
|
-
# Assignment right-hand sides that witness a fresh allocation. `.
|
|
26
|
+
# Assignment right-hand sides that witness a fresh allocation. `.dup` / `.clone` follow
|
|
27
27
|
# [ADR-76](../adr/76-effect-modeling-freeze-dup-shape-preservation.md)'s reading of `dup` as the
|
|
28
|
-
# allocation witness
|
|
29
|
-
|
|
28
|
+
# allocation witness; `.new` is {constructor?}'s, because only a class object's `new` is `Class#new`.
|
|
29
|
+
COPYING_SELECTORS = %i[dup clone].to_set.freeze
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
33
|
# The set of frame-owned local names in `body`, given the method's parameter names (a parameter is
|
|
34
|
-
# never frame-owned — the caller holds the same object, so mutating it is `mutate.instance`)
|
|
35
|
-
|
|
34
|
+
# never frame-owned — the caller holds the same object, so mutating it is `mutate.instance`) and
|
|
35
|
+
# whether `self` is a class there ({constructor?}).
|
|
36
|
+
def owned(body, parameter_names, singleton:)
|
|
36
37
|
return Set.new if body.nil?
|
|
37
38
|
|
|
38
39
|
assignments = {}
|
|
@@ -42,23 +43,48 @@ module Rigor
|
|
|
42
43
|
assignments.filter_map do |name, values|
|
|
43
44
|
next if escaped.include?(name) || parameter_names.include?(name)
|
|
44
45
|
|
|
45
|
-
name if values.all? { |value| allocation?(value) }
|
|
46
|
+
name if values.all? { |value| allocation?(value, singleton: singleton) }
|
|
46
47
|
end.to_set
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Whether `node` is an expression that allocates a fresh object this frame is the sole holder of.
|
|
50
|
-
|
|
51
|
+
# `singleton` is whether the enclosing unit's `self` is a class, as {constructor?} reads it.
|
|
52
|
+
def allocation?(node, singleton:)
|
|
51
53
|
case node
|
|
52
54
|
when Prism::ArrayNode, Prism::HashNode, Prism::StringNode, Prism::InterpolatedStringNode,
|
|
53
55
|
Prism::LambdaNode
|
|
54
56
|
true
|
|
55
57
|
when Prism::CallNode
|
|
56
|
-
|
|
58
|
+
COPYING_SELECTORS.include?(node.name) || constructor?(node, singleton: singleton) ||
|
|
59
|
+
unary_plus_string?(node)
|
|
57
60
|
else
|
|
58
61
|
false
|
|
59
62
|
end
|
|
60
63
|
end
|
|
61
64
|
|
|
65
|
+
# A `new` whose receiver the author wrote as a class object: a constant path, a `class` call
|
|
66
|
+
# (`self.class`, `other.class`), or `self` — explicit or implicit — in a singleton-method body. Any
|
|
67
|
+
# other receiver may be an object whose `new` only shares the name: an ActiveRecord association's
|
|
68
|
+
# `new` builds a record into the association's own target, where the caller can reach it, and a
|
|
69
|
+
# gem's method gives no edge that would carry that to the caller. `self` in an instance method is
|
|
70
|
+
# such a receiver too — an association extension's `new` is the association's.
|
|
71
|
+
def constructor?(node, singleton:)
|
|
72
|
+
return false unless node.name == :new
|
|
73
|
+
|
|
74
|
+
receiver = node.receiver
|
|
75
|
+
case receiver
|
|
76
|
+
when nil, Prism::SelfNode then singleton
|
|
77
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then true
|
|
78
|
+
when Prism::CallNode then class_call?(receiver)
|
|
79
|
+
else false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# `x.class` — `Kernel#class` answers the receiver's class, unless the receiver overrides it.
|
|
84
|
+
def class_call?(node)
|
|
85
|
+
node.name == :class && node.arguments.nil? && node.block.nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
62
88
|
# `+""` — the frozen-string-literal era's spelling of "a fresh mutable String".
|
|
63
89
|
def unary_plus_string?(node)
|
|
64
90
|
node.name == :+@ && node.receiver.is_a?(Prism::StringNode)
|
|
@@ -125,8 +151,8 @@ module Rigor
|
|
|
125
151
|
last.is_a?(Prism::LocalVariableReadNode) ? [last.name.to_s] : []
|
|
126
152
|
end
|
|
127
153
|
|
|
128
|
-
private_class_method :collect, :record_assignment, :record_escapes, :stored_value,
|
|
129
|
-
:trailing_reads
|
|
154
|
+
private_class_method :class_call?, :collect, :record_assignment, :record_escapes, :stored_value,
|
|
155
|
+
:note_read, :trailing_reads
|
|
130
156
|
end
|
|
131
157
|
end
|
|
132
158
|
end
|
|
@@ -12,6 +12,11 @@ module Rigor
|
|
|
12
12
|
# key, and `effects.attribution:` in `.rigor.yml` names one. A key the loader accepts and the scanner
|
|
13
13
|
# would never produce is a table that silently matches nothing.
|
|
14
14
|
module MethodKey
|
|
15
|
+
# #393 — the prefix a template unit's key carries (`Plugin::TemplateUnit::KEY_PREFIX`, repeated here
|
|
16
|
+
# rather than required so the effects layer keeps no dependency on the plugin layer; the two are
|
|
17
|
+
# pinned equal by a spec).
|
|
18
|
+
TEMPLATE_UNIT_PREFIX = "view:"
|
|
19
|
+
|
|
15
20
|
module_function
|
|
16
21
|
|
|
17
22
|
# @return `[owner, separator, selector]`, or nil when `key` is
|
|
@@ -35,6 +40,22 @@ module Rigor
|
|
|
35
40
|
def owner(key)
|
|
36
41
|
split(key)&.first
|
|
37
42
|
end
|
|
43
|
+
|
|
44
|
+
# #393 — a template unit's key: `view:users/show.html`. Deliberately NOT a method key — a view has
|
|
45
|
+
# no owner class and no selector — but {.split} cannot tell, because the format segment gives the
|
|
46
|
+
# string a dot: `owner("view:users/show.html")` answers `"view:users/show"`, a class name no run
|
|
47
|
+
# ever produced. Anything grouping keys by their envelope-bearing owner has to ask this first.
|
|
48
|
+
def template_unit?(key)
|
|
49
|
+
key.to_s.start_with?(TEMPLATE_UNIT_PREFIX)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The name an `effects.envelopes:` entry selects a key by: the owner class for a method key, and the
|
|
53
|
+
# unit key ITSELF for a template unit. A view is its own envelope subject — there is no class to
|
|
54
|
+
# hang the bound on, and `Runner#effect_sources` already knows which file the key came from, which
|
|
55
|
+
# is what a `match:` entry needs.
|
|
56
|
+
def envelope_owner(key)
|
|
57
|
+
template_unit?(key) ? key.to_s : owner(key)
|
|
58
|
+
end
|
|
38
59
|
end
|
|
39
60
|
end
|
|
40
61
|
end
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../inference/mutation_widening"
|
|
6
6
|
require_relative "label_set"
|
|
7
|
+
require_relative "local_ownership"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Effects
|
|
@@ -18,7 +19,9 @@ module Rigor
|
|
|
18
19
|
# proven lane is worse than a missing one — the proven lane is the one a verdict may read (ADR-5).
|
|
19
20
|
# 2. **Who owns the receiver?** `self` and its ivars are `mutate.self` (`mutate.static` in singleton
|
|
20
21
|
# context), a class variable is `mutate.static`, a parameter is `mutate.instance`, a frame-owned
|
|
21
|
-
# local is `mutate.local
|
|
22
|
+
# local is `mutate.local`, and so is a receiver that is itself an allocation (`{}.compare_by_identity`,
|
|
23
|
+
# `raw.dup.force_encoding(e)`) by the local rule's own witness ({LocalOwnership.allocation?}): each
|
|
24
|
+
# evaluation is a new object no caller holds yet. **Anything else answers nil**, and the caller records an
|
|
22
25
|
# `unknown-ownership` taint rather than a proven bare `mutate`: Ruby's ownership is a dataflow
|
|
23
26
|
# question, and a proven parent label on a fresh-but-unproven receiver would put findings on correct
|
|
24
27
|
# code (WD14).
|
|
@@ -26,18 +29,23 @@ module Rigor
|
|
|
26
29
|
# The only selectors a mutation may be claimed from without knowing the receiver's class.
|
|
27
30
|
UNIVERSAL_MUTATORS = %i[[]=].to_set.freeze
|
|
28
31
|
|
|
29
|
-
# `String`'s receiver-mutating surface. `Array` / `Hash` reuse the hand-audited sets the widening
|
|
30
|
-
# rules already maintain, cited rather than re-derived (ADR-103 WD3).
|
|
31
|
-
STRING_MUTATORS = %i[
|
|
32
|
-
<< concat replace insert prepend clear
|
|
33
|
-
upcase! downcase! capitalize! swapcase! reverse!
|
|
34
|
-
strip! lstrip! rstrip! chomp! chop! squeeze! succ! next!
|
|
35
|
-
sub! gsub! tr! tr_s! delete! slice! []=
|
|
36
|
-
].to_set.freeze
|
|
37
|
-
|
|
38
32
|
# `foo=`, and deliberately not `==` / `<=` / `!=` / `===`.
|
|
39
33
|
ATTRIBUTE_WRITER = /\A[a-z_][A-Za-z0-9_]*=\z/
|
|
40
34
|
|
|
35
|
+
# Every Hash method that mutates its receiver. The widening keeps two tables because they answer two different
|
|
36
|
+
# questions of a `HashShape` — `MutationWidening::HASH_MUTATORS` changes the pair set,
|
|
37
|
+
# `HashLookupMutation::MUTATORS` changes what a read of the pairs answers — but either is a write to the
|
|
38
|
+
# receiver, and that is all this asks: `h.compare_by_identity` raises `FrozenError` on a frozen hash as
|
|
39
|
+
# `h.clear` does (unless the hash already compares by identity, when it returns before the check), and a
|
|
40
|
+
# method whose one effect was `@h.compare_by_identity` did not read as mutating while this cited the first
|
|
41
|
+
# table alone.
|
|
42
|
+
#
|
|
43
|
+
# `rehash` is in neither table because it changes no read a shape can state: a shape's keys are literals, which
|
|
44
|
+
# a rebuild never merges. It still rebuilds the receiver's table in place, and it is the one name spelt here.
|
|
45
|
+
HASH_MUTATORS = (
|
|
46
|
+
Inference::MutationWidening::HASH_MUTATORS | Inference::HashLookupMutation::MUTATORS | Set[:rehash]
|
|
47
|
+
).freeze
|
|
48
|
+
|
|
41
49
|
LABELS = {
|
|
42
50
|
self_state: LabelSet.new(["mutate.self"]),
|
|
43
51
|
static: LabelSet.new(["mutate.static"]),
|
|
@@ -57,10 +65,12 @@ module Rigor
|
|
|
57
65
|
name = node.name
|
|
58
66
|
return true if UNIVERSAL_MUTATORS.include?(name) || ATTRIBUTE_WRITER.match?(name.to_s)
|
|
59
67
|
|
|
68
|
+
# The per-class sets are the hand-audited ones the widening rules maintain, cited rather than re-derived
|
|
69
|
+
# (ADR-103 WD3): a list kept here drifted from the widening's, and missed `force_encoding`.
|
|
60
70
|
case receiver_class
|
|
61
71
|
when "Array" then Inference::MutationWidening::ARRAY_MUTATORS.include?(name)
|
|
62
|
-
when "Hash" then
|
|
63
|
-
when "String" then
|
|
72
|
+
when "Hash" then HASH_MUTATORS.include?(name)
|
|
73
|
+
when "String" then Inference::StringMutation::MUTATORS.include?(name)
|
|
64
74
|
else false
|
|
65
75
|
end
|
|
66
76
|
end
|
|
@@ -78,6 +88,7 @@ module Rigor
|
|
|
78
88
|
@singleton ? :static : :self_state
|
|
79
89
|
when Prism::ClassVariableReadNode then :static
|
|
80
90
|
when Prism::LocalVariableReadNode then local_ownership(receiver.name.to_s)
|
|
91
|
+
else :local if LocalOwnership.allocation?(receiver, singleton: @singleton)
|
|
81
92
|
end
|
|
82
93
|
end
|
|
83
94
|
|