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
|
@@ -5,7 +5,9 @@ require "prism"
|
|
|
5
5
|
require_relative "../source/constant_path"
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
7
|
require_relative "attribution"
|
|
8
|
+
require_relative "callee_rule"
|
|
8
9
|
require_relative "catalog"
|
|
10
|
+
require_relative "definition_context"
|
|
9
11
|
require_relative "envelope_index"
|
|
10
12
|
require_relative "file_collection"
|
|
11
13
|
require_relative "label_set"
|
|
@@ -35,12 +37,62 @@ module Rigor
|
|
|
35
37
|
# Long by construction: the walk carries one `when` per Ruby construct that originates an effect, and
|
|
36
38
|
# splitting that table across classes would put the vocabulary in one file and the reasons in another.
|
|
37
39
|
class UnitScan # rubocop:disable Metrics/ClassLength
|
|
38
|
-
# `$~` and
|
|
39
|
-
#
|
|
40
|
-
|
|
40
|
+
# `$~` and `$_` are frame-local, not global state (#1363): Ruby keeps them in the special-variable slot
|
|
41
|
+
# of the body that runs them. The blocks that body creates reach the same slot, except the root block
|
|
42
|
+
# of a `Thread.new`, `Fiber.new` or `Ractor.new`, which has its own; a call into a method defined with
|
|
43
|
+
# `def` does not reach it. A read of one is not `global.read`, and a write (`$_ = line`, `$~ = nil`)
|
|
44
|
+
# binds only that slot, so it earns no label, as a local-variable write earns none — except in a
|
|
45
|
+
# `define_method` body (`@shared_slot`), which runs on the slot of the body that defined it and so
|
|
46
|
+
# shares it with every sibling defined there. The rest of the match family (`$&`, `` $` ``, `$'`, `$+`,
|
|
47
|
+
# `$1`…) are Prism nodes of their own that this scan does not colour, and none can be assigned.
|
|
48
|
+
FRAME_LOCAL_GLOBALS = %i[$~ $_].to_set.freeze
|
|
49
|
+
|
|
50
|
+
# `$!` and `$@` are the exception being rescued and its backtrace. They are not frame-local: a read
|
|
51
|
+
# reaches the dynamically enclosing rescue clause, whichever frame runs it — a caller's, or a callee's
|
|
52
|
+
# that yields to a block written here. So `$!` is an implicit argument of the running call rather than
|
|
53
|
+
# program state, and a read of it is not `global.read`. A read of `$@` reads that exception's backtrace
|
|
54
|
+
# as `e.backtrace` would, and a read of an object's state is never labelled. A write is another matter
|
|
55
|
+
# and stays `gvar-write`: `$@ = bt` changes that object, which the rescuing frame observes
|
|
56
|
+
# (`rescue => e` sees the new backtrace). Ruby refuses `$! = x`.
|
|
57
|
+
RESCUED_EXCEPTION_GLOBALS = %i[$! $@].to_set.freeze
|
|
58
|
+
|
|
59
|
+
# The globals whose read is not `global.read`.
|
|
60
|
+
UNCOLOURED_READS = (FRAME_LOCAL_GLOBALS | RESCUED_EXCEPTION_GLOBALS).freeze
|
|
41
61
|
|
|
42
62
|
REFLECTIVE_SEND = %i[send public_send __send__].to_set.freeze
|
|
43
63
|
|
|
64
|
+
# The constructs under which a call may not run (#1048). Only the `responds:` bit reads this, and
|
|
65
|
+
# only to refuse to record a response the body might not perform: `render :edit if x` leaves the
|
|
66
|
+
# other path taking Rails' implicit render.
|
|
67
|
+
#
|
|
68
|
+
# The modifier forms need no entry of their own — Prism spells `render :x if y` as an ordinary
|
|
69
|
+
# `IfNode` — and `RescueNode` is the rescue CLAUSE rather than the body it guards, so a `render` in
|
|
70
|
+
# the `begin` half of `begin … rescue … end` is at depth zero, which is right: that half runs.
|
|
71
|
+
#
|
|
72
|
+
# A `LambdaNode` is here for the same reason and needs no exemption: `@after = -> { redirect_to
|
|
73
|
+
# "/" }` stores a response rather than performing one, and the body may never be called at all.
|
|
74
|
+
#
|
|
75
|
+
# A **block** is branching too, but is answered by {#branching?} rather than listed here, because
|
|
76
|
+
# only the CALL that owns it can say whether it is one. `User.transaction { redirect_to "/" }`,
|
|
77
|
+
# `[1].each { redirect_to "/" }` and `x.presence&.then { … }` all contain a call the body may not
|
|
78
|
+
# make, and recording a response from one would drop the implicit-render edge AND leave the unit
|
|
79
|
+
# reading exhaustive — the combination this bit exists to prevent.
|
|
80
|
+
BRANCHING = [
|
|
81
|
+
Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode,
|
|
82
|
+
Prism::WhileNode, Prism::UntilNode, Prism::ForNode,
|
|
83
|
+
Prism::RescueNode, Prism::RescueModifierNode, Prism::AndNode, Prism::OrNode,
|
|
84
|
+
Prism::LambdaNode
|
|
85
|
+
].to_set.freeze
|
|
86
|
+
|
|
87
|
+
# The one block a response may be recorded through: `respond_to`'s own. It is the format dispatcher
|
|
88
|
+
# rather than a conditional, and it always runs — but its ARMS do not, so `f.html { … }` is an
|
|
89
|
+
# ordinary branching block and a `render json:` in the `f.json` arm no longer stands the `f.html`
|
|
90
|
+
# arm's implicit render down (#1048). The dispatcher's other half is the per-arm conventional edge
|
|
91
|
+
# (#1071): each `format.<fmt>` arm the body spells renders `<action>.<fmt>` where the arm's block
|
|
92
|
+
# does not answer on every path of its own body, exactly as an uninstrumented action renders
|
|
93
|
+
# `<action>.html`. The arms are read off the same syntax that marks the block transparent.
|
|
94
|
+
DISPATCH_SELECTORS = %i[respond_to respond_with].to_set.freeze
|
|
95
|
+
|
|
44
96
|
# Selectors a per-class POSTURE default must never answer for, because a more specific reading of
|
|
45
97
|
# the same site exists and would be swallowed: `send` and friends are the `dynamic-send` taint, and
|
|
46
98
|
# `call` is the `opaque-callable` one. An explicit ROW still wins (`Fiddle::Function#call` is
|
|
@@ -85,9 +137,10 @@ module Rigor
|
|
|
85
137
|
# declaration wherever it appears: in a class body it is the only way that method exists, and inside
|
|
86
138
|
# another method it is a definition the enclosing method performs (`mutate.static`) rather than code
|
|
87
139
|
# the enclosing method contains. A non-literal name has no key to file the block under, so it stays
|
|
88
|
-
# contained in the enclosing method and this returns nil.
|
|
140
|
+
# contained in the enclosing method and this returns nil. Which side the method lands on is the
|
|
141
|
+
# {DefinitionContext}'s to say.
|
|
89
142
|
#
|
|
90
|
-
# @return `[name,
|
|
143
|
+
# @return `[name, body, parameters]`
|
|
91
144
|
def self.define_method_unit(node)
|
|
92
145
|
return nil unless node.name == :define_method && node.receiver.nil?
|
|
93
146
|
|
|
@@ -97,11 +150,13 @@ module Rigor
|
|
|
97
150
|
block = node.block
|
|
98
151
|
return nil unless block.is_a?(Prism::BlockNode)
|
|
99
152
|
|
|
100
|
-
[first.unescaped,
|
|
153
|
+
[first.unescaped, block.body, block.parameters]
|
|
101
154
|
end
|
|
102
155
|
|
|
103
|
-
# @param
|
|
104
|
-
# `
|
|
156
|
+
# @param context — the {DefinitionContext} the body runs under. Its singleton bit — whether the
|
|
157
|
+
# unit's `self` is a class object (`def self.x`, a `def` in `class << self`) — is the axis that
|
|
158
|
+
# separates `mutate.self` from `mutate.static` on an ivar write; the rest says where the units
|
|
159
|
+
# nested in the body land
|
|
105
160
|
# @param block_parameter — the unit's `&blk` parameter name, if any; a call on it is
|
|
106
161
|
# forwarding, not an opaque callable
|
|
107
162
|
# @param calls — node-identity table of {Collector::CallRecord}s
|
|
@@ -113,10 +168,24 @@ module Rigor
|
|
|
113
168
|
# @param method_name — this unit's own selector — what a `super` in its body names as
|
|
114
169
|
# the target the propagator resolves above `owner_class` (#446). With no name to state, a `super`
|
|
115
170
|
# taints instead.
|
|
116
|
-
|
|
171
|
+
# @param non_public — whether the class body declared this unit `private` or `protected`
|
|
172
|
+
# (#1048). Only a UNIT callee rule reads it, and only to decline: Rails' `action_methods` is a
|
|
173
|
+
# controller's PUBLIC instance methods, so a private helper is never implicitly rendered — while
|
|
174
|
+
# a project that happens to ship a template of the same name would otherwise hand that
|
|
175
|
+
# template's effects to the helper.
|
|
176
|
+
# @param shared_slot — whether the body is a `define_method` block, which runs on the special-variable
|
|
177
|
+
# slot of the body that defined it rather than on one of its own (#1363). A write to a
|
|
178
|
+
# {FRAME_LOCAL_GLOBALS} name there reaches every sibling defined in that body, so it stays
|
|
179
|
+
# `global.write`.
|
|
180
|
+
def initialize(context:, parameters:, block_parameter:, owned_locals:, calls:, # rubocop:disable Metrics/ParameterLists
|
|
117
181
|
attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
|
|
118
|
-
plugin_facts: PluginFacts.empty, owner_class: nil, method_name: nil
|
|
182
|
+
plugin_facts: PluginFacts.empty, owner_class: nil, method_name: nil,
|
|
183
|
+
non_public: false, shared_slot: false)
|
|
184
|
+
singleton = context.singleton?
|
|
119
185
|
@singleton = singleton
|
|
186
|
+
# The context at the walk's current position. It starts as the body's own and moves only inside a
|
|
187
|
+
# block or `class << self` body that rebinds `self`; the unit's singleton bit does not move with it.
|
|
188
|
+
@context = context
|
|
120
189
|
@block_parameter = block_parameter
|
|
121
190
|
@calls = calls
|
|
122
191
|
@attribution = attribution
|
|
@@ -124,6 +193,8 @@ module Rigor
|
|
|
124
193
|
@plugin_facts = plugin_facts
|
|
125
194
|
@owner_class = owner_class
|
|
126
195
|
@method_name = method_name
|
|
196
|
+
@non_public = non_public ? true : false
|
|
197
|
+
@shared_slot = shared_slot ? true : false
|
|
127
198
|
@mutation = MutationClassifier.new(
|
|
128
199
|
singleton: singleton, parameters: parameters, owned_locals: owned_locals
|
|
129
200
|
)
|
|
@@ -133,10 +204,44 @@ module Rigor
|
|
|
133
204
|
@edges = []
|
|
134
205
|
@nested = []
|
|
135
206
|
@delegates_upward = false
|
|
207
|
+
# #1048 — whether a plugin row marked `responds:` fired **unconditionally** in this unit, i.e.
|
|
208
|
+
# the body supplied the framework's answer on every path through it. It is what stands a UNIT
|
|
209
|
+
# callee rule down: an action that called `render :edit` or `redirect_to` outright did not take
|
|
210
|
+
# Rails' implicit render.
|
|
211
|
+
#
|
|
212
|
+
# Conditional does not count, and that is the whole of the bit's care. `redirect_to root_path if
|
|
213
|
+
# @user.nil?` leaves the other path taking the implicit render, so a unit that recorded the
|
|
214
|
+
# response there would drop the template edge AND read exhaustive — the one combination an
|
|
215
|
+
# effect summary may never produce. {@conditional} counts the branching ancestors the walk is
|
|
216
|
+
# inside; only a depth of zero answers.
|
|
217
|
+
@responded = false
|
|
218
|
+
@conditional = 0
|
|
219
|
+
@transparent_blocks = Set.new.compare_by_identity
|
|
220
|
+
# #1071 — a `respond_to` dispatcher's format arms, read for the per-arm conventional edge that
|
|
221
|
+
# replaced the single `html` unit edge. Each arm is one `format.<fmt>` call made on the
|
|
222
|
+
# dispatcher's block parameter, and an arm's own block is a branch the scan walks with the same
|
|
223
|
+
# {@conditional} bit scoped to it: a `responds:` row fired at the arm's top level stands THAT
|
|
224
|
+
# arm's conventional template down, exactly as a unit-level response stands the implicit render
|
|
225
|
+
# down. `@arms_by_block` maps an arm block node to its arm so the walk can open the scope;
|
|
226
|
+
# `@arm_stack` holds the open ones with the depth their body's top level sits at;
|
|
227
|
+
# `@block_stack` is every open block node, so an arm call is recognised by its enclosing block
|
|
228
|
+
# being one of {@transparent_blocks}; `@dispatch_top_level` remembers whether a dispatcher was
|
|
229
|
+
# spelled at the unit's top level, which is what lets the unit rule stand down for the standard
|
|
230
|
+
# responder while an action whose `respond_to` is inside a branch still keeps its plain implicit
|
|
231
|
+
# render.
|
|
232
|
+
@format_arms = []
|
|
233
|
+
@arms_by_block = {}.compare_by_identity
|
|
234
|
+
@arm_stack = []
|
|
235
|
+
@block_stack = []
|
|
236
|
+
@dispatch_top_level = false
|
|
237
|
+
# #391 — set only where a site could not carry the bit on an edge; see {#record_edge}.
|
|
238
|
+
@unclaimed = false
|
|
136
239
|
end
|
|
137
240
|
|
|
138
241
|
# Units discovered inside this one — a nested `def`, or a `define_method` with a literal name whose
|
|
139
|
-
# block becomes that method's body. Each is `[name,
|
|
242
|
+
# block becomes that method's body. Each is `[name, body context, body_node, parameters_node,
|
|
243
|
+
# shared_slot]`, the context being the {DefinitionContext}'s answer at the definition and `shared_slot`
|
|
244
|
+
# true for the `define_method` block (see {#initialize}). One it cannot place is omitted.
|
|
140
245
|
attr_reader :nested
|
|
141
246
|
|
|
142
247
|
# Whether this body reaches `super` — an override that delegates upward still runs whatever the
|
|
@@ -153,13 +258,22 @@ module Rigor
|
|
|
153
258
|
# Walks `body` and returns `[Summary, edges]`.
|
|
154
259
|
def run(body)
|
|
155
260
|
walk(body)
|
|
261
|
+
apply_unit_callees
|
|
156
262
|
summary = Summary.new(
|
|
157
263
|
bundles: @bundles, declared_bundles: @declared_bundles,
|
|
158
|
-
exhaustive: @causes.empty?, causes: @causes
|
|
264
|
+
exhaustive: @causes.empty?, causes: @causes, unclaimed: @unclaimed
|
|
159
265
|
)
|
|
160
266
|
[summary, @edges]
|
|
161
267
|
end
|
|
162
268
|
|
|
269
|
+
# One `format.<fmt>` arm of a `respond_to` dispatcher (#1071): the format it spells, and whether a
|
|
270
|
+
# plugin `responds:` row fired unconditionally in the arm's own block. `responded` is mutated by the
|
|
271
|
+
# walk (a Struct, unlike the Data rows the rules produce) and `conventional?` is the any/all
|
|
272
|
+
# exclusion — those two arms serve every format and can name no single template.
|
|
273
|
+
FormatArm = Struct.new(:format, :responded, keyword_init: true) do
|
|
274
|
+
def conventional? = format != "any" && format != "all"
|
|
275
|
+
end
|
|
276
|
+
|
|
163
277
|
private
|
|
164
278
|
|
|
165
279
|
def add(origin, labels)
|
|
@@ -183,21 +297,66 @@ module Rigor
|
|
|
183
297
|
return if unit_boundary?(node)
|
|
184
298
|
|
|
185
299
|
visit(node)
|
|
186
|
-
node
|
|
300
|
+
return walk_children(node) unless branching?(node)
|
|
301
|
+
|
|
302
|
+
@conditional += 1
|
|
303
|
+
arm = enter_arm_block(node)
|
|
304
|
+
walk_children(node)
|
|
305
|
+
@arm_stack.pop if arm
|
|
306
|
+
@conditional -= 1
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The children, with the block stack maintained: an arm call is recognised by its innermost
|
|
310
|
+
# enclosing block being one of the dispatcher's transparent ones, so every `BlockNode` the walk
|
|
311
|
+
# descends into has to be on the stack while its body runs (#1071).
|
|
312
|
+
def walk_children(node)
|
|
313
|
+
if node.is_a?(Prism::BlockNode)
|
|
314
|
+
@block_stack.push(node)
|
|
315
|
+
node.rigor_each_child { |child| walk(child) }
|
|
316
|
+
@block_stack.pop
|
|
317
|
+
elsif DefinitionContext.rebinds?(node)
|
|
318
|
+
walk_rebinding(node)
|
|
319
|
+
else
|
|
320
|
+
node.rigor_each_child { |child| walk(child) }
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# The children of a node one of which may run under another {DefinitionContext} — a `class_eval`
|
|
325
|
+
# block, a `class << self` body — so a unit nested there is keyed where Ruby defines it.
|
|
326
|
+
def walk_rebinding(node)
|
|
327
|
+
outer = @context
|
|
328
|
+
node.rigor_each_child do |child|
|
|
329
|
+
@context = outer.for_child(node, child)
|
|
330
|
+
walk(child)
|
|
331
|
+
end
|
|
332
|
+
@context = outer
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Whether the walk is entering a construct whose body may not run. A `BlockNode` answers from the
|
|
336
|
+
# call that owns it, which {#visit_call} has already marked — `walk` visits a node before its
|
|
337
|
+
# children, so the mark is always in place by the time the block is reached.
|
|
338
|
+
def branching?(node)
|
|
339
|
+
return !@transparent_blocks.include?(node) if node.is_a?(Prism::BlockNode)
|
|
340
|
+
|
|
341
|
+
BRANCHING.include?(node.class)
|
|
187
342
|
end
|
|
188
343
|
|
|
189
344
|
# A nested unit is recorded and NOT descended into: its body belongs to its own summary, and the
|
|
190
|
-
# enclosing method gets only the `mutate.static` of having defined it.
|
|
345
|
+
# enclosing method gets only the `mutate.static` of having defined it. One the {DefinitionContext}
|
|
346
|
+
# cannot place is not descended into either, and belongs to no summary.
|
|
191
347
|
def unit_boundary?(node)
|
|
192
348
|
case node
|
|
193
349
|
when Prism::DefNode
|
|
194
|
-
|
|
350
|
+
context = @context.def_body(node)
|
|
351
|
+
@nested << [node.name.to_s, context, node.body, node.parameters, false] if context
|
|
195
352
|
true
|
|
196
353
|
when Prism::CallNode
|
|
197
354
|
declared = self.class.define_method_unit(node)
|
|
198
355
|
return false unless declared
|
|
199
356
|
|
|
200
|
-
|
|
357
|
+
name, body, parameters = declared
|
|
358
|
+
context = @context.module_call_body
|
|
359
|
+
@nested << [name, context, body, parameters, true] if context
|
|
201
360
|
add(DEFINE_METHOD, MUTATE_STATIC)
|
|
202
361
|
true
|
|
203
362
|
else
|
|
@@ -211,10 +370,12 @@ module Rigor
|
|
|
211
370
|
when Prism::XStringNode, Prism::InterpolatedXStringNode
|
|
212
371
|
add(XSTRING, IO_PROCESS)
|
|
213
372
|
when Prism::GlobalVariableReadNode
|
|
214
|
-
add(GVAR_READ, GLOBAL_READ) unless
|
|
373
|
+
add(GVAR_READ, GLOBAL_READ) unless UNCOLOURED_READS.include?(node.name)
|
|
215
374
|
when Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode,
|
|
216
|
-
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode
|
|
217
|
-
|
|
375
|
+
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
376
|
+
Prism::GlobalVariableTargetNode
|
|
377
|
+
# A target is a multiple assignment's, a `for` loop's or a `rescue =>` clause's.
|
|
378
|
+
add(GVAR_WRITE, GLOBAL_WRITE) unless !@shared_slot && FRAME_LOCAL_GLOBALS.include?(node.name)
|
|
218
379
|
when Prism::ClassVariableReadNode
|
|
219
380
|
add(CVAR_READ, GLOBAL_READ)
|
|
220
381
|
when Prism::ClassVariableWriteNode, Prism::ClassVariableOperatorWriteNode,
|
|
@@ -256,6 +417,8 @@ module Rigor
|
|
|
256
417
|
end
|
|
257
418
|
|
|
258
419
|
def visit_call(node)
|
|
420
|
+
mark_transparent_block(node)
|
|
421
|
+
record_format_arm(node)
|
|
259
422
|
record = @calls[node]
|
|
260
423
|
attribute(node, record)
|
|
261
424
|
plugin = attribute_plugin(node, record)
|
|
@@ -270,6 +433,64 @@ module Rigor
|
|
|
270
433
|
visit_block_argument(node)
|
|
271
434
|
end
|
|
272
435
|
|
|
436
|
+
# `respond_to do |format| … end` — the block that is a dispatcher rather than a branch. Marked by
|
|
437
|
+
# identity, because a `BlockNode` cannot name the call it belongs to and two structurally equal
|
|
438
|
+
# blocks in one body are two blocks (#1048). A dispatcher spelled at the unit's top level is also
|
|
439
|
+
# remembered (#1071): its arms answer for the whole body, so the conventional `<action>.html` unit
|
|
440
|
+
# edge stands down; a dispatcher inside a branch does not cover the fall-through path.
|
|
441
|
+
def mark_transparent_block(node)
|
|
442
|
+
return unless node.receiver.nil? && DISPATCH_SELECTORS.include?(node.name)
|
|
443
|
+
|
|
444
|
+
block = node.block
|
|
445
|
+
@transparent_blocks << block if block.is_a?(Prism::BlockNode)
|
|
446
|
+
@dispatch_top_level ||= true if @conditional.zero?
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# #1071 — one `format.<fmt>` arm of the enclosing `respond_to`, when the formatter is the block's
|
|
450
|
+
# own parameter: the edge its arm answers for is the conventional `<action>.<fmt>` template, and the
|
|
451
|
+
# walk needs the arm registered and its block (if any) mapped before it descends into that block's
|
|
452
|
+
# body. A call made on anything else in the block is not an arm; a formatter nobody assigned a
|
|
453
|
+
# parameter is a dispatcher whose arms cannot be read, and the unit rule simply keeps its old
|
|
454
|
+
# single answer.
|
|
455
|
+
def record_format_arm(node)
|
|
456
|
+
enclosing = @block_stack.last
|
|
457
|
+
return unless enclosing.is_a?(Prism::BlockNode) && @transparent_blocks.include?(enclosing)
|
|
458
|
+
|
|
459
|
+
parameter = block_parameter(enclosing)
|
|
460
|
+
return if parameter.nil?
|
|
461
|
+
return unless node.receiver.is_a?(Prism::LocalVariableReadNode) && node.receiver.name.to_s == parameter
|
|
462
|
+
|
|
463
|
+
arm = FormatArm.new(format: node.name.to_s)
|
|
464
|
+
@format_arms << arm
|
|
465
|
+
block = node.block
|
|
466
|
+
@arms_by_block[block] = arm if block.is_a?(Prism::BlockNode)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# The name of a dispatcher block's first required parameter — `|format|` in the ordinary shape. A
|
|
470
|
+
# `respond_to` whose block spells no parameter (or a splat, or a destructure) has no formatter the
|
|
471
|
+
# walk can name arms on.
|
|
472
|
+
def block_parameter(block)
|
|
473
|
+
params = block.parameters
|
|
474
|
+
return nil unless params.is_a?(Prism::BlockParametersNode)
|
|
475
|
+
|
|
476
|
+
required = params.parameters&.requireds
|
|
477
|
+
return nil if required.nil? || required.empty?
|
|
478
|
+
|
|
479
|
+
first = required.first
|
|
480
|
+
first.name.to_s if first.respond_to?(:name)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Opens an arm scope for the walk: the arm's own block is a branch like any other, and its body's
|
|
484
|
+
# top level sits at the current {@conditional} depth — the value a `responds:` row must fire at for
|
|
485
|
+
# the ARM to count as answered outright.
|
|
486
|
+
def enter_arm_block(node)
|
|
487
|
+
arm = @arms_by_block[node]
|
|
488
|
+
return nil unless arm
|
|
489
|
+
|
|
490
|
+
@arm_stack << [arm, @conditional]
|
|
491
|
+
arm
|
|
492
|
+
end
|
|
493
|
+
|
|
273
494
|
# The **plugin stratum** (#387; ADR-103 WD6 / WD10): what the plugin that models a framework says
|
|
274
495
|
# this call does.
|
|
275
496
|
#
|
|
@@ -290,15 +511,122 @@ module Rigor
|
|
|
290
511
|
row = plugin_row(node, record)
|
|
291
512
|
return nil if row.nil?
|
|
292
513
|
|
|
514
|
+
@responded = true if row.responds && @conditional.zero?
|
|
515
|
+
mark_arm_responded(row)
|
|
516
|
+
edged = callee_edge_taken?(node, row)
|
|
293
517
|
labels = row.narrow ? Narrowing.apply(row.narrow, node) : row.labels
|
|
294
|
-
|
|
518
|
+
# A `narrow:` that narrowed to nothing says the call does nothing, and a call that does nothing
|
|
519
|
+
# taints nothing. A row that declares no labels at all is a different statement — since #1048 a
|
|
520
|
+
# row may contribute an EDGE and no label — so it falls through to the taints rather than
|
|
521
|
+
# returning here.
|
|
522
|
+
return row if row.narrow && (labels.nil? || labels.empty?)
|
|
523
|
+
|
|
524
|
+
add_declared(Origin.plugin(row.key), labels) unless labels.nil? || labels.empty?
|
|
525
|
+
record_plugin_taints(row, edged)
|
|
526
|
+
row
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# The `responds:` bit scoped to an OPEN format arm (#1071): a row fired at the arm's body top
|
|
530
|
+
# level, so the arm never falls through to its conventional `<action>.<fmt>` template. A deeper
|
|
531
|
+
# response does not stand the arm's own fall-through down, exactly as a conditional response does
|
|
532
|
+
# not stand a unit's implicit render down.
|
|
533
|
+
def mark_arm_responded(row)
|
|
534
|
+
return unless row.responds
|
|
295
535
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
536
|
+
arm, depth = @arm_stack.last
|
|
537
|
+
return if arm.nil?
|
|
538
|
+
|
|
539
|
+
arm.responded = true if @conditional == depth
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# A row may discharge AND still taint: `render` states exactly what the CONTROLLER does and says
|
|
543
|
+
# nothing about the template. Where a {CalleeRule} named the template the taint rides the EDGE
|
|
544
|
+
# instead (#1048), so a render that reaches a real unit clears it and one that reaches nothing
|
|
545
|
+
# keeps it — decided by the propagator, which is the only thing that knows.
|
|
546
|
+
def record_plugin_taints(row, edged)
|
|
547
|
+
taint(row.taint, row.key) if row.taint && !edged
|
|
300
548
|
taint("plugin-attribution", row.key) unless row.discharge?
|
|
301
|
-
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# #1048 — the call-graph edge a framework method IS, when the plugin's row named a {CalleeRule}.
|
|
552
|
+
# `render :show` inside `UsersController` runs `view:users/show.html`, which is a unit in the same
|
|
553
|
+
# table; the rule reads the call's argument literals and nothing else, and answers nil for anything
|
|
554
|
+
# it cannot settle — a computed target, an unmodelled option, a receiver whose class names no view
|
|
555
|
+
# directory. Nil leaves the site exactly as it was, taint included.
|
|
556
|
+
#
|
|
557
|
+
# @return whether an edge took the row's taint with it
|
|
558
|
+
def callee_edge_taken?(node, row)
|
|
559
|
+
return false if row.callee.nil? || !CalleeRule.site_rule?(row.callee)
|
|
560
|
+
|
|
561
|
+
callee = CalleeRule.site(row.callee, node, owner_class: @owner_class, unit_key: @method_name,
|
|
562
|
+
fallbacks: row.callee_fallbacks)
|
|
563
|
+
return false if callee.nil?
|
|
564
|
+
|
|
565
|
+
@edges << FileCollection::Edge.new(
|
|
566
|
+
receiver_class: callee.receiver, kind: :singleton, selector: callee.selector, self_call: false,
|
|
567
|
+
taint_if_unresolved: row.taint ? [row.taint, row.key].freeze : nil,
|
|
568
|
+
fallback_selectors: callee.fallbacks
|
|
569
|
+
)
|
|
570
|
+
!row.taint.nil?
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# The UNIT callee rules (#1048): an edge a plugin declares for a body that made no call at all.
|
|
574
|
+
# Rails' implicit render is the whole of the case — an action that falls off its end renders
|
|
575
|
+
# `<controller>/<action>` — so the producing fact is the ABSENCE of a `responds:` row, which only a
|
|
576
|
+
# finished unit scan can observe.
|
|
577
|
+
#
|
|
578
|
+
# Contributes an edge and nothing else — no label, no taint — which is what keeps it from being
|
|
579
|
+
# wrong about a method that is not an action at all.
|
|
580
|
+
#
|
|
581
|
+
# Two units it never applies to, and both are the same false positive twice: a `private` /
|
|
582
|
+
# `protected` member (Rails' `action_methods` is public only) and a `def` nested inside another
|
|
583
|
+
# method. A project with `app/views/users/card.html.erb` and a `private def card` would otherwise
|
|
584
|
+
# hand that template's `io.db.write` to the helper.
|
|
585
|
+
#
|
|
586
|
+
# #1071 widens the answer per `respond_to` arm: a unit that spelled a dispatcher renders one
|
|
587
|
+
# conventional `<action>.<fmt>` template per arm rather than one `<action>.html`, so the row is
|
|
588
|
+
# applied once per arm the walk read, each answered outright (a `responds:` row at the arm's own
|
|
589
|
+
# top level) or degenerate (`any` / `all`) standing its arm's edge down. The `<action>.html`
|
|
590
|
+
# answer survives only for a dispatcher the walk could not name arms on, or one nested under a
|
|
591
|
+
# branch — the plain fall-through still exists on the path that skips it.
|
|
592
|
+
def apply_unit_callees
|
|
593
|
+
return if @responded || @singleton || @non_public
|
|
594
|
+
return if @owner_class.nil? || @method_name.nil?
|
|
595
|
+
|
|
596
|
+
rows = @plugin_facts.unit_callee_rows
|
|
597
|
+
return if rows.empty?
|
|
598
|
+
|
|
599
|
+
rows.each do |row|
|
|
600
|
+
next unless @plugin_facts.descends_from?(@owner_class, row.receiver)
|
|
601
|
+
|
|
602
|
+
apply_unit_callee_row(row)
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# One unit callee row across the unit's format arms: nil targets the plain `<action>.html` implicit
|
|
607
|
+
# render, and one application per arm reaches that arm's own `<action>.<fmt>` convention (#1071) —
|
|
608
|
+
# an arm that answered outright, or one serving every format (`any` / `all`), names nothing.
|
|
609
|
+
def apply_unit_callee_row(row)
|
|
610
|
+
return apply_unit_callee(row, nil) if @format_arms.empty?
|
|
611
|
+
|
|
612
|
+
apply_unit_callee(row, nil) unless @dispatch_top_level
|
|
613
|
+
@format_arms.each { |arm| apply_unit_callee(row, arm) }
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Applies one unit callee row's rule for one target format.
|
|
617
|
+
# @return the {Callee} the edge was recorded for, or nil when the arm (or the format) names nothing
|
|
618
|
+
def apply_unit_callee(row, arm)
|
|
619
|
+
return nil if arm && !arm.conventional?
|
|
620
|
+
return nil if arm&.responded
|
|
621
|
+
|
|
622
|
+
format = arm&.format
|
|
623
|
+
callee = CalleeRule.unit(row.callee, owner_class: @owner_class, unit_key: @method_name, format: format)
|
|
624
|
+
return nil if callee.nil?
|
|
625
|
+
|
|
626
|
+
@edges << FileCollection::Edge.new(
|
|
627
|
+
receiver_class: callee.receiver, kind: :singleton, selector: callee.selector, self_call: false
|
|
628
|
+
)
|
|
629
|
+
callee
|
|
302
630
|
end
|
|
303
631
|
|
|
304
632
|
def plugin_row(node, record)
|
|
@@ -545,7 +873,11 @@ module Rigor
|
|
|
545
873
|
selector = literal_selector(node.arguments&.arguments&.first)
|
|
546
874
|
return taint("dynamic-send") unless selector
|
|
547
875
|
|
|
548
|
-
|
|
876
|
+
# A literal `send` is an ordinary call and nothing bounded it, so it is unclaimed on the same
|
|
877
|
+
# terms as {#record_edge}'s.
|
|
878
|
+
push_edge(record, selector, node.receiver.nil?,
|
|
879
|
+
unclaimed: true, constant_receiver: constant_receiver?(node.receiver))
|
|
880
|
+
@unclaimed = true if !node.receiver.nil? && !edge_recordable?(record)
|
|
549
881
|
end
|
|
550
882
|
|
|
551
883
|
def literal_selector(node)
|
|
@@ -571,7 +903,14 @@ module Rigor
|
|
|
571
903
|
# such calls are ordinary inherited ones the catalogue simply has no row for.
|
|
572
904
|
def record_edge(node, record, bound = nil)
|
|
573
905
|
self_call = node.receiver.nil?
|
|
574
|
-
|
|
906
|
+
# #391 — an uncatalogued, unbounded site is a site whose callee nobody described. Whether that
|
|
907
|
+
# matters is the propagator's to decide: if the edge lands on a project definition the closure
|
|
908
|
+
# reads that definition's own summary and the site is fully accounted for. So the bit travels ON
|
|
909
|
+
# the edge, and the unit is marked directly only where there is no edge to carry it.
|
|
910
|
+
unclaimed = bound.nil?
|
|
911
|
+
push_edge(record, node.name.to_s, self_call,
|
|
912
|
+
unclaimed: unclaimed, constant_receiver: constant_receiver?(node.receiver))
|
|
913
|
+
@unclaimed = true if unclaimed && !self_call && !edge_recordable?(record)
|
|
575
914
|
return unless self_call && (record.nil? || !record.resolved)
|
|
576
915
|
# An envelope on this unit's own class for the very selector the dispatcher declined is the
|
|
577
916
|
# project declaring the method and stating its bound; a discharging plugin row on the framework
|
|
@@ -583,14 +922,30 @@ module Rigor
|
|
|
583
922
|
taint("unresolved-self-call", node.name.to_s)
|
|
584
923
|
end
|
|
585
924
|
|
|
586
|
-
def push_edge(record, selector, self_call)
|
|
587
|
-
return
|
|
925
|
+
def push_edge(record, selector, self_call, unclaimed: false, constant_receiver: false)
|
|
926
|
+
return unless edge_recordable?(record)
|
|
588
927
|
|
|
589
928
|
@edges << FileCollection::Edge.new(
|
|
590
|
-
receiver_class: record.receiver_class, kind: record.kind, selector: selector,
|
|
929
|
+
receiver_class: record.receiver_class, kind: record.kind, selector: selector,
|
|
930
|
+
self_call: self_call, unclaimed: unclaimed, constant_receiver: constant_receiver
|
|
591
931
|
)
|
|
592
932
|
end
|
|
593
933
|
|
|
934
|
+
# Whether the AUTHOR wrote this receiver as a constant path (#1039). The edge is otherwise keyed on
|
|
935
|
+
# the receiver's type, which cannot tell `Base.new` from `self.class.new` — the same `Singleton[Base]`
|
|
936
|
+
# in both — and the two reach different constructors. Everything else, a bare `self` included, is a
|
|
937
|
+
# receiver whose run-time class may be a subclass.
|
|
938
|
+
def constant_receiver?(receiver)
|
|
939
|
+
receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# Whether {#push_edge} has a receiver to key an edge on. A call whose receiver the typer never
|
|
943
|
+
# named carries nothing the propagator can resolve, so an unclaimed site of that shape marks the
|
|
944
|
+
# unit directly instead of handing the bit to an edge that will not exist (#391).
|
|
945
|
+
def edge_recordable?(record)
|
|
946
|
+
!record.nil? && !record.receiver_class.nil?
|
|
947
|
+
end
|
|
948
|
+
|
|
594
949
|
def visit_block_argument(node)
|
|
595
950
|
block = node.block
|
|
596
951
|
return unless block.is_a?(Prism::BlockArgumentNode)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Effects
|
|
7
|
+
# The `private` / `protected` members a class body declared, read straight off its statement list in
|
|
8
|
+
# source order (#1048).
|
|
9
|
+
#
|
|
10
|
+
# One reader, and it only ever declines: a UNIT {CalleeRule} rule. Rails' `action_methods` is a
|
|
11
|
+
# controller's **public** instance methods, so a private helper is never implicitly rendered as
|
|
12
|
+
# `<controller>/<helper>` — while a project that happens to ship a template of that name would
|
|
13
|
+
# otherwise hand the template's effects to the helper. `private def card` beside
|
|
14
|
+
# `app/views/users/card.html.erb` is the measured shape.
|
|
15
|
+
#
|
|
16
|
+
# Deliberately shallow and deliberately syntactic. It reads the class body's own top level, which is
|
|
17
|
+
# where all three spellings appear in practice, and answers "public" for anything it cannot see —
|
|
18
|
+
# `send(:private, :card)`, a visibility change in a `class_eval`, a concern that privatises on
|
|
19
|
+
# include. Declining to decline leaves today's behaviour intact, which is the right direction for a
|
|
20
|
+
# bit whose only power is to remove an edge.
|
|
21
|
+
module Visibility
|
|
22
|
+
# The markers whose ARGUMENT form names members. `public` is one of them and **subtracts**:
|
|
23
|
+
# `private; def reopened; end; public :reopened` is a public action, and an answer that only ever
|
|
24
|
+
# grew would mark it private and silently drop its implicit-render edge.
|
|
25
|
+
HIDING = %i[private protected].freeze
|
|
26
|
+
SHOWING = :public
|
|
27
|
+
|
|
28
|
+
NONE = [].freeze
|
|
29
|
+
private_constant :NONE
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# The method names `body` — a class or module body — declared non-public.
|
|
34
|
+
def non_public_names(body)
|
|
35
|
+
statements = body.respond_to?(:body) ? Array(body.body) : NONE
|
|
36
|
+
names = Set.new
|
|
37
|
+
region = false
|
|
38
|
+
statements.each do |statement|
|
|
39
|
+
case statement
|
|
40
|
+
# A `def self.x` is not the instance method a later `def x` defines, and the two share a name.
|
|
41
|
+
# Recording the singleton would mark the instance method private, which is the same false
|
|
42
|
+
# negative from the other side.
|
|
43
|
+
when Prism::DefNode then names << statement.name.to_s if region && statement.receiver.nil?
|
|
44
|
+
when Prism::CallNode
|
|
45
|
+
region = region_after(statement, region)
|
|
46
|
+
apply_targets(names, statement)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
names
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether a **bare** `private` / `protected` / `public` opened or closed the region. A marker with
|
|
53
|
+
# arguments names specific members instead and leaves the region exactly as it was, which is what
|
|
54
|
+
# lets `private def a` sit above a `public` region without closing it.
|
|
55
|
+
def region_after(node, region)
|
|
56
|
+
return region unless bare?(node)
|
|
57
|
+
|
|
58
|
+
case node.name
|
|
59
|
+
when :private, :protected then true
|
|
60
|
+
when :public then false
|
|
61
|
+
else region
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# `private def foo` / `private :foo, :bar`, and their inverse `public def foo` / `public :foo`.
|
|
66
|
+
def apply_targets(names, node)
|
|
67
|
+
return unless node.receiver.nil?
|
|
68
|
+
return names.merge(argument_names(node)) if HIDING.include?(node.name)
|
|
69
|
+
return unless node.name == SHOWING
|
|
70
|
+
|
|
71
|
+
argument_names(node).each { |name| names.delete(name) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def argument_names(node)
|
|
75
|
+
arguments = node.arguments&.arguments
|
|
76
|
+
return NONE if arguments.nil? || arguments.empty?
|
|
77
|
+
|
|
78
|
+
arguments.flat_map { |argument| names_in(argument) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# One argument's names. The array form (`private %i[a b]`, `private [:a, :b]`) is read because it
|
|
82
|
+
# is ordinary and costs a line; a **splat** (`private(*names)`) is not, because the names are a
|
|
83
|
+
# value rather than syntax, and such a member reads as public — the direction that leaves
|
|
84
|
+
# today's behaviour intact rather than guessing.
|
|
85
|
+
def names_in(argument)
|
|
86
|
+
case argument
|
|
87
|
+
when Prism::DefNode then [argument.name.to_s]
|
|
88
|
+
when Prism::SymbolNode, Prism::StringNode then [argument.unescaped]
|
|
89
|
+
when Prism::ArrayNode then argument.elements.flat_map { |element| names_in(element) }
|
|
90
|
+
else NONE
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def bare?(node)
|
|
95
|
+
node.receiver.nil? && node.arguments.nil? && node.block.nil?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private_class_method :region_after, :apply_targets, :argument_names, :names_in, :bare?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|