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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
|
+
require_relative "callee_rule"
|
|
5
6
|
require_relative "entry_points"
|
|
6
7
|
require_relative "label_set"
|
|
7
8
|
require_relative "registry"
|
|
@@ -65,7 +66,14 @@ module Rigor
|
|
|
65
66
|
# `discharge` is the RESOLVED grant, not the manifest's request: {.build} has already demoted a
|
|
66
67
|
# non-first-party plugin's `true` to `false` and recorded a warning, so nothing downstream has to
|
|
67
68
|
# re-ask who may discharge.
|
|
68
|
-
|
|
69
|
+
# `callee` names a {CalleeRule} rule (#1048) and `responds` marks a row whose call supplies the
|
|
70
|
+
# unit's answer, so a unit rule on the same receiver stands down. `receiver` is carried because a
|
|
71
|
+
# UNIT rule has no call node to read one off. `callee_fallbacks` is the plugin's lookup-order table
|
|
72
|
+
# for the rule's answer (#1065), carried verbatim to the edge the rule records.
|
|
73
|
+
Row = Data.define(:key, :labels, :narrow, :discharge, :within, :taint, :plugin_id, :receiver,
|
|
74
|
+
:callee, :callee_fallbacks, :responds) do
|
|
75
|
+
def initialize(receiver: nil, callee: nil, callee_fallbacks: nil, responds: false, **) = super
|
|
76
|
+
|
|
69
77
|
def discharge? = discharge
|
|
70
78
|
end
|
|
71
79
|
|
|
@@ -73,6 +81,10 @@ module Rigor
|
|
|
73
81
|
# plugin pointed.
|
|
74
82
|
Edge = Data.define(:target, :receiver, :selector, :plugin_id)
|
|
75
83
|
|
|
84
|
+
# The rows whose `callee:` names a {CalleeRule::UNIT_RULES} rule — applied once per unit by
|
|
85
|
+
# {UnitScan}, from the unit's own owner and key rather than from a call node.
|
|
86
|
+
attr_reader :unit_callee_rows
|
|
87
|
+
|
|
76
88
|
def self.empty
|
|
77
89
|
@empty ||= new(contributions: [], superclasses: NO_ROWS, includes: NO_ROWS)
|
|
78
90
|
end
|
|
@@ -110,6 +122,7 @@ module Rigor
|
|
|
110
122
|
@self_rows = {}
|
|
111
123
|
@result_rows = {}
|
|
112
124
|
@edges = []
|
|
125
|
+
@unit_callee_rows = []
|
|
113
126
|
@labels_by_owner = {}
|
|
114
127
|
@entry_points = []
|
|
115
128
|
@declared_ancestry = {}
|
|
@@ -146,9 +159,7 @@ module Rigor
|
|
|
146
159
|
# @param owner — the receiver's class name as the syntax or the typer named it
|
|
147
160
|
# @param singleton — whether the call is `Owner.selector`
|
|
148
161
|
def class_row(owner, singleton, selector)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
bucket = @class_rows[singleton]
|
|
162
|
+
bucket = owner.nil? ? nil : @class_rows[singleton]
|
|
152
163
|
return nil if bucket.nil?
|
|
153
164
|
|
|
154
165
|
ancestry(owner).each do |candidate|
|
|
@@ -161,9 +172,7 @@ module Rigor
|
|
|
161
172
|
# The row colouring `path`'s `selector`, where `path` is a receiver expression (`"Rails.cache"`).
|
|
162
173
|
# Exact — a receiver path names one object and has no ancestry to walk.
|
|
163
174
|
def path_row(path, selector)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@path_rows[path]&.[](selector)
|
|
175
|
+
path.nil? ? nil : @path_rows[path]&.[](selector)
|
|
167
176
|
end
|
|
168
177
|
|
|
169
178
|
# The row colouring `path`'s `selector` for a receiver rooted at implicit self (`"self.flash.now"`),
|
|
@@ -171,11 +180,8 @@ module Rigor
|
|
|
171
180
|
# `owner_class`'s project ancestry — a receiver-less `session` outside a controller is a different
|
|
172
181
|
# `session`.
|
|
173
182
|
def self_path_row(path, selector, owner_class)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
row = @self_rows[path]&.[](selector)
|
|
177
|
-
return nil if row.nil?
|
|
178
|
-
return nil unless descends_from?(owner_class, row.within)
|
|
183
|
+
row = path.nil? ? nil : @self_rows[path]&.[](selector)
|
|
184
|
+
return nil if row.nil? || !descends_from?(owner_class, row.within)
|
|
179
185
|
|
|
180
186
|
row
|
|
181
187
|
end
|
|
@@ -201,9 +207,7 @@ module Rigor
|
|
|
201
207
|
|
|
202
208
|
# Whether `class_name` is `ancestor`, or reaches it through the project's own `class … <` lines.
|
|
203
209
|
def descends_from?(class_name, ancestor)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
ancestry(class_name).include?(ancestor)
|
|
210
|
+
!class_name.nil? && ancestry(class_name).include?(ancestor)
|
|
207
211
|
end
|
|
208
212
|
|
|
209
213
|
# `registry` extended with every plugin's `effect_labels:`, each under its own owner so
|
|
@@ -225,10 +229,10 @@ module Rigor
|
|
|
225
229
|
note_root_demotion(contribution)
|
|
226
230
|
(@labels_by_owner[contribution.owner] ||= []).concat(contribution.labels)
|
|
227
231
|
contribution.attributions.each { |entry| absorb_attribution(contribution, entry) }
|
|
228
|
-
contribution.edges.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
end
|
|
232
|
+
@edges.concat(contribution.edges.map do |entry|
|
|
233
|
+
Edge.new(target: entry.target, receiver: entry.receiver, selector: entry.method,
|
|
234
|
+
plugin_id: contribution.id)
|
|
235
|
+
end)
|
|
232
236
|
@entry_points.concat(contribution.entry_points)
|
|
233
237
|
contribution.ancestry.each { |entry| absorb_ancestry(contribution, entry) }
|
|
234
238
|
end
|
|
@@ -267,8 +271,22 @@ module Rigor
|
|
|
267
271
|
def absorb_attribution(contribution, entry)
|
|
268
272
|
row = Row.new(key: entry.key, labels: LabelSet.new(entry.labels), narrow: entry.narrow,
|
|
269
273
|
discharge: discharge_granted?(contribution, entry), within: entry.within,
|
|
270
|
-
taint: entry.taint, plugin_id: contribution.id
|
|
274
|
+
taint: entry.taint, plugin_id: contribution.id, receiver: entry.receiver,
|
|
275
|
+
callee: callee_rule_for(contribution, entry), responds: entry.responds,
|
|
276
|
+
callee_fallbacks: entry.callee_fallbacks)
|
|
271
277
|
(bucket_for(entry)[entry.receiver] ||= {})[entry.method.to_s] = row
|
|
278
|
+
@unit_callee_rows << row if row.callee && CalleeRule.unit_rule?(row.callee)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# #1048 — a `callee:` naming a rule the engine does not implement is dropped with a warning rather
|
|
282
|
+
# than silently doing nothing, for the reason {#note_root_demotion} gives: a contribution that
|
|
283
|
+
# quietly evaporates reads to its author as the feature being broken.
|
|
284
|
+
def callee_rule_for(contribution, entry)
|
|
285
|
+
return entry.callee if entry.callee.nil? || CalleeRule.known?(entry.callee)
|
|
286
|
+
|
|
287
|
+
@warnings << "plugin #{contribution.id.inspect} names an unknown callee rule " \
|
|
288
|
+
"#{entry.callee.inspect} on #{entry.key}; the row contributes no edge"
|
|
289
|
+
nil
|
|
272
290
|
end
|
|
273
291
|
|
|
274
292
|
# Which index a row lands in, from its receiver spelling.
|
|
@@ -351,22 +369,16 @@ module Rigor
|
|
|
351
369
|
bucket.sort.map do |receiver, rows|
|
|
352
370
|
[receiver,
|
|
353
371
|
rows.sort.map do |selector, row|
|
|
354
|
-
[selector, row.labels.to_a, row.narrow, row.discharge, row.within, row.taint
|
|
372
|
+
[selector, row.labels.to_a, row.narrow, row.discharge, row.within, row.taint,
|
|
373
|
+
row.callee, row.callee_fallbacks, row.responds]
|
|
355
374
|
end]
|
|
356
375
|
end
|
|
357
376
|
end
|
|
358
377
|
|
|
359
378
|
def finalize
|
|
360
|
-
@class_rows.each_value { |bucket| bucket.each_value(&:freeze) }
|
|
361
|
-
@
|
|
362
|
-
@class_rows.freeze
|
|
363
|
-
@path_rows.each_value(&:freeze)
|
|
364
|
-
@path_rows.freeze
|
|
365
|
-
@self_rows.each_value(&:freeze)
|
|
366
|
-
@self_rows.freeze
|
|
367
|
-
@result_rows.each_value(&:freeze)
|
|
368
|
-
@result_rows.freeze
|
|
369
|
-
@edges.freeze
|
|
379
|
+
@class_rows.each_value { |bucket| bucket.each_value(&:freeze).freeze }
|
|
380
|
+
[@path_rows, @self_rows, @result_rows].each { |bucket| bucket.each_value(&:freeze).freeze }
|
|
381
|
+
[@class_rows, @edges, @unit_callee_rows].each(&:freeze)
|
|
370
382
|
@labels_by_owner.each_value(&:uniq!)
|
|
371
383
|
@labels_by_owner.reject! { |_, labels| labels.empty? }
|
|
372
384
|
@labels_by_owner.freeze
|
|
@@ -48,12 +48,15 @@ module Rigor
|
|
|
48
48
|
# @param collection — the run's merged per-file collections
|
|
49
49
|
# @param discharge — the `effects.tolerated:` policy the undischarged lane is computed
|
|
50
50
|
# under; {Discharge.none} makes the two lanes equal.
|
|
51
|
-
|
|
51
|
+
# @param declined_unit_keys — unit keys the run KNOWS a source file for and has no summary for
|
|
52
|
+
# (#1065): a template a plugin claimed and declined. An edge's fallback list is not tried past one
|
|
53
|
+
# of them, because the file the framework runs is the one that produced nothing.
|
|
54
|
+
def propagate(collection, discharge: Discharge.none, declined_unit_keys: nil)
|
|
52
55
|
return EffectTable.empty if collection.summaries.empty?
|
|
53
56
|
|
|
54
57
|
summaries = collection.summaries
|
|
55
58
|
state = seed(summaries, discharge)
|
|
56
|
-
edges = resolve_edges(collection, state)
|
|
59
|
+
edges = resolve_edges(collection, state, declined_unit_keys)
|
|
57
60
|
iterate(state, edges)
|
|
58
61
|
EffectTable.new(build_entries(summaries, edges, state))
|
|
59
62
|
rescue StandardError
|
|
@@ -62,12 +65,14 @@ module Rigor
|
|
|
62
65
|
|
|
63
66
|
# `{caller_key => [callee_key]}`, sorted and de-duplicated. Seeds the `unresolved-super` taint in
|
|
64
67
|
# the same pass, because whether a `super` resolved is exactly what this resolution answers.
|
|
65
|
-
def resolve_edges(collection, state)
|
|
66
|
-
index = Index.new(collection)
|
|
68
|
+
def resolve_edges(collection, state, declined_unit_keys = nil)
|
|
69
|
+
index = Index.new(collection, declined_unit_keys: declined_unit_keys)
|
|
67
70
|
collection.edges.each_with_object({}) do |(caller_key, list), out|
|
|
68
71
|
targets = list.flat_map do |edge|
|
|
69
|
-
resolved = index.
|
|
72
|
+
resolved = index.resolve(edge)
|
|
70
73
|
taint_unresolved_super(state, caller_key, edge) if edge.super_call && resolved.empty?
|
|
74
|
+
taint_unresolved_callee(state, caller_key, edge) if edge.taint_if_unresolved && resolved.empty?
|
|
75
|
+
mark_unclaimed(state, caller_key) if edge.unclaimed && !edge.super_call && !index.owner_resolved?(edge)
|
|
71
76
|
resolved
|
|
72
77
|
end.uniq.sort
|
|
73
78
|
out[caller_key] = targets.freeze unless targets.empty?
|
|
@@ -95,6 +100,47 @@ module Rigor
|
|
|
95
100
|
entry[:causes] << ["unresolved-super", edge.selector].freeze
|
|
96
101
|
end
|
|
97
102
|
|
|
103
|
+
# #1048 — a `callee:` edge a plugin's row produced, whose named unit is not in the table: the
|
|
104
|
+
# template the render site pointed at was never analysed, so the row's `template-not-analysed` cause
|
|
105
|
+
# is seeded here after all.
|
|
106
|
+
#
|
|
107
|
+
# The taint is ADDED on failure rather than subtracted on success, which is what keeps the fixpoint
|
|
108
|
+
# monotone and the answer independent of visit order. It is also why a row whose rule declined
|
|
109
|
+
# outright — a computed `render foo`, a `render json:` — never reaches this: {UnitScan} tainted that
|
|
110
|
+
# site directly and recorded no edge, so nothing about an unresolvable render changed.
|
|
111
|
+
def taint_unresolved_callee(state, caller_key, edge)
|
|
112
|
+
entry = state[caller_key]
|
|
113
|
+
return if entry.nil?
|
|
114
|
+
|
|
115
|
+
entry[:exhaustive] = false
|
|
116
|
+
# Frozen here rather than trusted from the edge: `Marshal.load` of a `Data` bypasses
|
|
117
|
+
# `initialize`, so a pooled worker's collection restores the pair unfrozen while a sequential
|
|
118
|
+
# one has the scan's frozen original. A cause travels into a `Set` shared by the whole fixpoint,
|
|
119
|
+
# and the two paths must hand it the same value. `#freeze` on an already-frozen array is free.
|
|
120
|
+
entry[:causes] << edge.taint_if_unresolved.freeze
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# #391 — an edge nothing bounded whose receiver's OWN ancestry holds no project definition: the
|
|
124
|
+
# callee's footprint was described by nobody, so the closure is "what the analyzer read", not
|
|
125
|
+
# "what the method does".
|
|
126
|
+
#
|
|
127
|
+
# The question is deliberately {Index#owner_resolved?} and NOT "did this edge resolve to nothing".
|
|
128
|
+
# An ordinary edge also joins every project subclass override of the selector (the closed-world
|
|
129
|
+
# join of ADR-103 WD4), so a receiver typed as a gem class a project subclass happens to override
|
|
130
|
+
# resolves non-empty while the receiver's own dispatch target is still undescribed — `B.run` on a
|
|
131
|
+
# `Base2 < Vendor::Client` reaches `Vendor::Client#run` at run time whatever `Sub2#run` does.
|
|
132
|
+
#
|
|
133
|
+
# Decided HERE for the same reason the `super` taint is: only the merged ancestry can say whether
|
|
134
|
+
# the edge resolves. Unlike that taint it is not a cause and does not touch exhaustiveness — every
|
|
135
|
+
# existing consumer reads exactly what it read before. Its one reader is sig-gen's emission, which
|
|
136
|
+
# must not write `%a{pure}` about a callee nobody described.
|
|
137
|
+
def mark_unclaimed(state, caller_key)
|
|
138
|
+
entry = state[caller_key]
|
|
139
|
+
return if entry.nil?
|
|
140
|
+
|
|
141
|
+
entry[:unclaimed] = true
|
|
142
|
+
end
|
|
143
|
+
|
|
98
144
|
# Causes are carried as a Set through the fixpoint and flattened back to a sorted Array in
|
|
99
145
|
# {build_entries}. A Set is what {absorb} needs: unioning one along an edge must cost the source's
|
|
100
146
|
# size and allocate NOTHING when it adds nothing, and the array-concat-and-uniq it replaces
|
|
@@ -105,7 +151,8 @@ module Rigor
|
|
|
105
151
|
proven: summary.proven,
|
|
106
152
|
undischarged: discharge.inert? ? summary.proven : discharge.undischarged(summary.bundles),
|
|
107
153
|
declared: summary.declared,
|
|
108
|
-
exhaustive: summary.exhaustive?, causes: Set.new(summary.causes)
|
|
154
|
+
exhaustive: summary.exhaustive?, causes: Set.new(summary.causes),
|
|
155
|
+
unclaimed: summary.unclaimed?
|
|
109
156
|
}
|
|
110
157
|
end
|
|
111
158
|
end
|
|
@@ -164,6 +211,10 @@ module Rigor
|
|
|
164
211
|
target[:exhaustive] = false
|
|
165
212
|
changed = true
|
|
166
213
|
end
|
|
214
|
+
if !target[:unclaimed] && source[:unclaimed]
|
|
215
|
+
target[:unclaimed] = true
|
|
216
|
+
changed = true
|
|
217
|
+
end
|
|
167
218
|
causes = target[:causes]
|
|
168
219
|
source[:causes].each { |cause| changed = true if causes.add?(cause) }
|
|
169
220
|
changed
|
|
@@ -190,25 +241,31 @@ module Rigor
|
|
|
190
241
|
declared: closed[:declared],
|
|
191
242
|
exhaustive: closed[:exhaustive],
|
|
192
243
|
causes: closed[:causes].sort_by { |cause, detail| [cause, detail.to_s] }.freeze,
|
|
193
|
-
edges: edges.fetch(key, NO_EDGES)
|
|
244
|
+
edges: edges.fetch(key, NO_EDGES),
|
|
245
|
+
unclaimed: closed[:unclaimed]
|
|
194
246
|
)
|
|
195
247
|
end
|
|
196
248
|
end
|
|
197
249
|
|
|
198
|
-
private_class_method :resolve_edges, :taint_unresolved_super, :
|
|
199
|
-
:
|
|
250
|
+
private_class_method :resolve_edges, :taint_unresolved_super, :taint_unresolved_callee,
|
|
251
|
+
:mark_unclaimed, :seed, :iterate,
|
|
252
|
+
:reverse_edges, :absorb, :join_lane, :build_entries
|
|
200
253
|
|
|
201
254
|
# The class graph a run's collections describe, and the edge resolution over it. Built once per
|
|
202
255
|
# propagation; every lookup is a Hash read.
|
|
203
256
|
class Index
|
|
204
|
-
def initialize(collection)
|
|
257
|
+
def initialize(collection, declined_unit_keys: nil)
|
|
205
258
|
@summaries = collection.summaries
|
|
259
|
+
@declined = declined_unit_keys.nil? || declined_unit_keys.empty? ? nil : declined_unit_keys.to_a.to_set
|
|
206
260
|
@superclasses = collection.superclasses
|
|
207
261
|
@includes = collection.includes
|
|
208
262
|
@classes = build_classes(collection)
|
|
209
263
|
@descendants = build_descendants(collection.superclasses)
|
|
210
264
|
@descendant_closures = {}
|
|
211
265
|
@targets = {}
|
|
266
|
+
# #391 — `{memo key => whether the receiver's own ancestry answered}`, filled by
|
|
267
|
+
# {#call_targets} so {#owner_resolved?} costs a Hash read rather than a second ancestor walk.
|
|
268
|
+
@owner_resolved = {}
|
|
212
269
|
end
|
|
213
270
|
|
|
214
271
|
# Every project method key `edge` may reach: the definition its ancestry resolves to, plus every
|
|
@@ -218,17 +275,237 @@ module Rigor
|
|
|
218
275
|
# one such tuple is asked for once per call site in the project. `ApplicationRecord#save` alone is
|
|
219
276
|
# thousands of sites on a Rails app, each of which used to re-walk the whole subclass forest.
|
|
220
277
|
def targets_for(edge)
|
|
221
|
-
@targets[
|
|
278
|
+
@targets[memo_key(edge)] ||= begin
|
|
222
279
|
separator = edge.kind == :singleton ? "." : "#"
|
|
223
|
-
|
|
280
|
+
key = memo_key(edge)
|
|
281
|
+
if edge.super_call
|
|
282
|
+
super_targets(edge, separator)
|
|
283
|
+
else
|
|
284
|
+
constructor_targets(edge, key) || call_targets(edge, key, separator)
|
|
285
|
+
end
|
|
224
286
|
end
|
|
225
287
|
end
|
|
226
288
|
|
|
289
|
+
# {#targets_for}, then — only where that answered nothing — the first of the edge's
|
|
290
|
+
# `fallback_selectors` that resolves (#1065), spelled as the same edge with that selector. A unit
|
|
291
|
+
# under the requested key therefore always wins and a fallback never joins a second target beside
|
|
292
|
+
# it: the lookup runs ONE template, and an edge to both would put a label on the caller that no
|
|
293
|
+
# execution of the render produces. Empty when every candidate fails, which is what lets the
|
|
294
|
+
# caller seed `taint_if_unresolved` exactly as before.
|
|
295
|
+
# A requested key the run **declined** — a template whose file exists and whose plugin produced no
|
|
296
|
+
# unit — stops there. "No unit answers" and "no such template" are different facts, and only the
|
|
297
|
+
# second licenses the framework's next candidate: an `_x.js.haml` beside an `_x.html.erb` is run by
|
|
298
|
+
# Action View as Haml, so joining the ERB unit would be a label no execution produces.
|
|
299
|
+
def resolve(edge)
|
|
300
|
+
resolved = targets_for(edge)
|
|
301
|
+
return resolved unless resolved.empty? && edge.fallback_selectors
|
|
302
|
+
return NO_TARGETS if declined?(edge)
|
|
303
|
+
|
|
304
|
+
fallback_targets(edge)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Whether the receiver's OWN ancestry holds a project definition of the selector — the half of
|
|
308
|
+
# {#targets_for} the closed-world subclass join hides (#391).
|
|
309
|
+
#
|
|
310
|
+
# `targets_for` answers "which project bodies may this edge reach", and for an effect closure the
|
|
311
|
+
# join is right: Ruby has no `final`, so a subclass override really can run. It is the wrong
|
|
312
|
+
# question for "is this callee described at all". A receiver typed as a class the project only
|
|
313
|
+
# subclasses dispatches into that class's own implementation, which lives in a gem or in core;
|
|
314
|
+
# that a project subclass overrides the same selector says nothing about it. Only emission asks
|
|
315
|
+
# this, and only to decline.
|
|
316
|
+
def owner_resolved?(edge)
|
|
317
|
+
targets_for(edge)
|
|
318
|
+
@owner_resolved.fetch(memo_key(edge), false)
|
|
319
|
+
end
|
|
320
|
+
|
|
227
321
|
private
|
|
228
322
|
|
|
229
|
-
def
|
|
323
|
+
def declined?(edge)
|
|
324
|
+
!@declined.nil? && @declined.include?("#{edge.receiver_class}.#{edge.selector}")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def fallback_targets(edge)
|
|
328
|
+
edge.fallback_selectors.each do |selector|
|
|
329
|
+
resolved = targets_for(edge.with(selector: selector, fallback_selectors: nil))
|
|
330
|
+
return resolved unless resolved.empty?
|
|
331
|
+
end
|
|
332
|
+
NO_TARGETS
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def memo_key(edge)
|
|
336
|
+
[edge.receiver_class, edge.kind, edge.selector, edge.super_call, edge.constant_receiver]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
NEW_SELECTOR = "new"
|
|
340
|
+
INITIALIZE_SELECTOR = "initialize"
|
|
341
|
+
private_constant :NEW_SELECTOR, :INITIALIZE_SELECTOR
|
|
342
|
+
|
|
343
|
+
# Class objects whose `new` is Ruby's own reflective constructor rather than a project class's
|
|
344
|
+
# (#1039). `Class.new` allocates an anonymous class and runs `Class#initialize`, NOT the
|
|
345
|
+
# `#initialize` of whatever the project happens to have named `Class`; the same holds for
|
|
346
|
+
# `Module.new`, and for `Struct.new` / `Data.define`, which build a class rather than an instance.
|
|
347
|
+
# Listed by name rather than left to the project-known guard below, because a project that reopens
|
|
348
|
+
# `Class` at all would otherwise turn every `Class.new` in it into a call on that reopening.
|
|
349
|
+
RESERVED_CONSTRUCTOR_OWNERS = %w[Class Module Struct Data].freeze
|
|
350
|
+
private_constant :RESERVED_CONSTRUCTOR_OWNERS
|
|
351
|
+
|
|
352
|
+
# Superclass spellings whose `#initialize` is Ruby's own and empty. A project chain that ends here
|
|
353
|
+
# — or ends implicitly, with no `<` at all — has no constructor body anywhere, which is what
|
|
354
|
+
# {#empty_constructor?} has to establish before it may say ∅ rather than "undescribed".
|
|
355
|
+
EMPTY_CONSTRUCTOR_ROOTS = %w[Object BasicObject].freeze
|
|
356
|
+
private_constant :EMPTY_CONSTRUCTOR_ROOTS
|
|
357
|
+
|
|
358
|
+
# #1039 — `Const.new` on a class the project defines, resolved to that class's `#initialize`.
|
|
359
|
+
#
|
|
360
|
+
# The collector records the call as `(Const, :singleton, "new")`, because that is what the typer
|
|
361
|
+
# saw; nothing in the project defines `Const.new`, so the edge resolved to nothing and every
|
|
362
|
+
# constructor body stayed out of its caller's closure. `new` is the one selector in Ruby whose
|
|
363
|
+
# dispatch target is spelled under a different key, and this is the rewrite: the same ancestor walk
|
|
364
|
+
# {#resolve_owner} performs, on the instance side, for `initialize`.
|
|
365
|
+
#
|
|
366
|
+
# Three boundaries, and each of them can only narrow:
|
|
367
|
+
#
|
|
368
|
+
# - a project `def self.new` **wins**. It is the definition `Const.new` actually reaches, and a
|
|
369
|
+
# constructor that overrides `new` is the case where `#initialize` is not the answer. Resolved
|
|
370
|
+
# through the singleton ancestry, so an inherited `self.new` wins too.
|
|
371
|
+
# - the receiver must be a class the project defines, and must not be one of
|
|
372
|
+
# {RESERVED_CONSTRUCTOR_OWNERS}. `Class.new { … }` is not a constructor call on a project class
|
|
373
|
+
# and must never reach a project `#initialize`; a gem class the project only calls `new` on is
|
|
374
|
+
# undescribed exactly as it was, and stays unclaimed.
|
|
375
|
+
# - the closed-world subclass join is **dropped for a written constant receiver, and only for one**.
|
|
376
|
+
# `Base.new` names the class object it constructs, so `Sub#initialize` cannot run and joining it
|
|
377
|
+
# would put a proven label on the caller that no execution of that site can produce — the `super`
|
|
378
|
+
# argument (see {#super_targets}) rather than the ordinary-call one. But the edge is keyed on the
|
|
379
|
+
# receiver's TYPE, and `self.class.new`, a `Singleton[Base]` local's `.new` and a receiver-less
|
|
380
|
+
# `new` in a singleton body produce the identical tuple while genuinely constructing a subclass.
|
|
381
|
+
# {FileCollection::Edge#constant_receiver} is what tells them apart, and every shape that is not
|
|
382
|
+
# a written constant keeps the join — over `Sub#initialize` and over a subclass `Sub.new` alike.
|
|
383
|
+
# - an ancestry the scanner could not read ({FileCollection::OPAQUE_ANCESTOR} — a non-constant
|
|
384
|
+
# superclass expression, an aliased `initialize`) declines, leaving the caller as unclaimed as it
|
|
385
|
+
# was before this rule existed.
|
|
386
|
+
#
|
|
387
|
+
# @return the targets, or `nil` when the rewrite does not apply and ordinary resolution should run
|
|
388
|
+
def constructor_targets(edge, memo_key)
|
|
389
|
+
return nil unless constructor_edge?(edge)
|
|
390
|
+
|
|
391
|
+
owner = resolve_owner(edge.receiver_class, "#", INITIALIZE_SELECTOR)
|
|
392
|
+
return nil unless owner || empty_constructor?(edge.receiver_class)
|
|
393
|
+
|
|
394
|
+
@owner_resolved[memo_key] = true
|
|
395
|
+
targets = owner ? [owner] : []
|
|
396
|
+
targets.concat(subclass_constructors(edge.receiver_class)) unless edge.constant_receiver
|
|
397
|
+
targets.uniq.freeze
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Whether the `new` rewrite applies to this edge at all — the guards of {#constructor_targets},
|
|
401
|
+
# each of which can only decline: a singleton `new` on a project class that is not one of Ruby's
|
|
402
|
+
# own class builders, whose singleton ancestry defines no `new` of its own, and whose constructor
|
|
403
|
+
# the scan could read both above it and (where the join applies) below it.
|
|
404
|
+
def constructor_edge?(edge)
|
|
405
|
+
edge.kind == :singleton && edge.selector == NEW_SELECTOR &&
|
|
406
|
+
!RESERVED_CONSTRUCTOR_OWNERS.include?(edge.receiver_class) &&
|
|
407
|
+
project_class?(edge.receiver_class) &&
|
|
408
|
+
resolve_owner(edge.receiver_class, ".", NEW_SELECTOR).nil? &&
|
|
409
|
+
!opaque_ancestry?(edge.receiver_class) &&
|
|
410
|
+
(edge.constant_receiver || !opaque_descendant?(edge.receiver_class))
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Every constructor a subclass of `class_name` supplies — its own `#initialize`, and its own
|
|
414
|
+
# `.new` where it overrides one. The closed-world join of step 2, spelled for the two keys a
|
|
415
|
+
# constructor can live under.
|
|
416
|
+
def subclass_constructors(class_name)
|
|
417
|
+
descendant_closure(class_name).each_with_object([]) do |subclass, keys|
|
|
418
|
+
keys << "#{subclass}##{INITIALIZE_SELECTOR}" if @summaries.key?("#{subclass}##{INITIALIZE_SELECTOR}")
|
|
419
|
+
keys << "#{subclass}.#{NEW_SELECTOR}" if @summaries.key?("#{subclass}.#{NEW_SELECTOR}")
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# The same question DOWNWARD, and only where the closed-world join applies. A subclass whose own
|
|
424
|
+
# constructor is unreadable — `class AliasedChild < Parent; alias initialize setup` — is reached by
|
|
425
|
+
# `self.class.new` in `Parent`, and {#subclass_constructors} would find no `AliasedChild#initialize`
|
|
426
|
+
# key and say nothing. The join is the whole reason this edge may construct a subclass at all, so an
|
|
427
|
+
# unreadable one in the closure declines the edge instead.
|
|
428
|
+
def opaque_descendant?(class_name)
|
|
429
|
+
descendant_closure(class_name).any? do |subclass|
|
|
430
|
+
@includes.fetch(subclass, []).include?(FileCollection::OPAQUE_ANCESTOR) ||
|
|
431
|
+
@superclasses.fetch(subclass, []).include?(FileCollection::OPAQUE_ANCESTOR)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Whether anything in `class_name`'s ancestry told the scanner its constructor is not readable from
|
|
436
|
+
# the source (#1039): a superclass expression that is not a constant path, or an `alias` that makes
|
|
437
|
+
# `initialize` another method. The sentinel rides the ancestry tables, so one walk finds it wherever
|
|
438
|
+
# in the chain it was recorded.
|
|
439
|
+
def opaque_ancestry?(class_name)
|
|
440
|
+
queue = [class_name]
|
|
441
|
+
seen = Set.new
|
|
442
|
+
until queue.empty?
|
|
443
|
+
current = queue.shift
|
|
444
|
+
next unless seen.add?(current)
|
|
445
|
+
return true if current == FileCollection::OPAQUE_ANCESTOR
|
|
446
|
+
|
|
447
|
+
queue.concat(@includes.fetch(current, []))
|
|
448
|
+
queue.concat(@superclasses.fetch(current, []))
|
|
449
|
+
end
|
|
450
|
+
false
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Whether `class_name`'s constructor is *known* to be `BasicObject#initialize`, whose footprint is
|
|
454
|
+
# ∅ (#1039).
|
|
455
|
+
#
|
|
456
|
+
# This is the design choice in the rewrite. "No `#initialize` in the ancestry" has two readings —
|
|
457
|
+
# the project defines none and none exists (a plain `class Bare; end`, constructed by Ruby's own
|
|
458
|
+
# empty constructor), or the project defines none and a gem's base class does. The first resolves
|
|
459
|
+
# to a known-empty definition: the edge contributes nothing AND leaves its caller claimed, because
|
|
460
|
+
# the callee was read — there is simply nothing in it. The second is undescribed and must keep
|
|
461
|
+
# marking its caller unclaimed. Only an ancestry that closes inside the project can tell them
|
|
462
|
+
# apart, so that is what this walks, and it answers `false` the moment the walk leaves.
|
|
463
|
+
#
|
|
464
|
+
# No summary row is invented for `BasicObject#initialize`. A row would be a *method of the
|
|
465
|
+
# project* in every table that lists them — the snapshot, `rigor effects`, the pure report — for a
|
|
466
|
+
# definition the project does not contain; the answer belongs to the edge, and the edge is already
|
|
467
|
+
# the thing {#owner_resolved?} is asked about.
|
|
468
|
+
#
|
|
469
|
+
# An `include` anywhere in the chain ends the walk conservatively. The collection's include table
|
|
470
|
+
# is a flat list of as-written *candidate spellings* (`include Foo` inside `module A` records both
|
|
471
|
+
# `A::Foo` and `Foo`), so it cannot say whether a class includes one project module or one gem
|
|
472
|
+
# module the project cannot see — and a module is free to define `initialize`. Such a class keeps
|
|
473
|
+
# exactly today's behaviour: its `new` edge resolves to nothing and its callers stay unclaimed.
|
|
474
|
+
# The table holds instance-side includes only. A module mixed into the singleton class, by
|
|
475
|
+
# `extend` or by an `include` inside `class << self`, is not in it, so a `new` such a module
|
|
476
|
+
# supplies is not seen, and the walk can answer `true` for a class whose `new` does work.
|
|
477
|
+
def empty_constructor?(class_name)
|
|
478
|
+
queue = [class_name]
|
|
479
|
+
seen = Set.new
|
|
480
|
+
until queue.empty?
|
|
481
|
+
current = queue.shift
|
|
482
|
+
next unless seen.add?(current)
|
|
483
|
+
return false unless project_class?(current)
|
|
484
|
+
return false unless @includes.fetch(current, []).empty?
|
|
485
|
+
|
|
486
|
+
parents = @superclasses.fetch(current, [])
|
|
487
|
+
next if parents.empty?
|
|
488
|
+
|
|
489
|
+
known = parents.find { |candidate| project_class?(candidate) }
|
|
490
|
+
return false if known.nil? && parents.none? { |candidate| EMPTY_CONSTRUCTOR_ROOTS.include?(candidate) }
|
|
491
|
+
|
|
492
|
+
queue << known if known
|
|
493
|
+
end
|
|
494
|
+
true
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Whether the project defines this class at all: it defines a method on it, declares its
|
|
498
|
+
# superclass, or declares what it includes. Any one of the three is a `class` body the scanner
|
|
499
|
+
# read, which is what licenses reading its silence about `initialize` as an answer.
|
|
500
|
+
def project_class?(class_name)
|
|
501
|
+
!class_name.nil? &&
|
|
502
|
+
(@classes.include?(class_name) || @superclasses.key?(class_name) || @includes.key?(class_name))
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def call_targets(edge, memo_key, separator)
|
|
230
506
|
targets = []
|
|
231
507
|
owner = resolve_owner(edge.receiver_class, separator, edge.selector)
|
|
508
|
+
@owner_resolved[memo_key] = !owner.nil?
|
|
232
509
|
targets << owner if owner
|
|
233
510
|
descendant_closure(edge.receiver_class).each do |subclass|
|
|
234
511
|
key = "#{subclass}#{separator}#{edge.selector}"
|
|
@@ -259,7 +536,7 @@ module Rigor
|
|
|
259
536
|
|
|
260
537
|
# Ancestry order mirrors the engine's: the class itself, the modules it includes, then its
|
|
261
538
|
# superclass, recursively. Cycle-guarded, because a project may declare one. Ancestry names
|
|
262
|
-
# arrive as as-written candidate lists (see `
|
|
539
|
+
# arrive as as-written candidate lists (see `AncestryRecorder#lexical_candidates`); every candidate is
|
|
263
540
|
# enqueued and the most-qualified one comes first, so the right constant wins the race and a
|
|
264
541
|
# spelling that names nothing simply matches no key.
|
|
265
542
|
def resolve_owner(class_name, separator, selector)
|
|
@@ -272,8 +549,10 @@ module Rigor
|
|
|
272
549
|
#
|
|
273
550
|
# The includes are instance-side only. `include M` puts `M#m` between the class and its
|
|
274
551
|
# superclass, which is exactly where `super` looks, while `M.m` is a singleton method `include`
|
|
275
|
-
# never contributes
|
|
276
|
-
# a `class << self` include, neither of which is collected, so
|
|
552
|
+
# never contributes. A singleton `super` that really does reach a module went through `extend` or
|
|
553
|
+
# a `class << self` include, neither of which is collected, so the walk steps over the module: it
|
|
554
|
+
# reaches the superclass chain's definition when there is one, without the module's labels, and
|
|
555
|
+
# resolves to nothing and taints when there is not.
|
|
277
556
|
def resolve_super(class_name, separator, selector)
|
|
278
557
|
queue = separator == "#" ? @includes.fetch(class_name, []).dup : []
|
|
279
558
|
queue.concat(@superclasses.fetch(class_name, []))
|
|
@@ -28,7 +28,7 @@ module Rigor
|
|
|
28
28
|
class InvalidLabelError < Error
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
DATA_PATH = File.expand_path("../../../data/effects/registry.yml", __dir__)
|
|
31
|
+
DATA_PATH = File.expand_path("../../../data/effects/registry.yml", __dir__).freeze
|
|
32
32
|
|
|
33
33
|
# How far a misspelling may be from a known label before {#suggest} declines to guess. Two
|
|
34
34
|
# edits catches a transposition or a dropped segment character ("io.nte", "nondet.tim")
|