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
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
#
|
|
32
32
|
# Every method carries an effect annotation, and the split down
|
|
33
33
|
# the middle of this file is the whole point of the Rails effect
|
|
34
|
-
# layer: a **builder** is `%a{pure}` because it issues nothing —
|
|
34
|
+
# layer: a **query builder** is `%a{pure}` because it issues nothing —
|
|
35
35
|
# `where` composes an Arel tree and returns a relation — while a
|
|
36
36
|
# **materializer** is `%a{rigor:v1:effect io.db.read}` because it
|
|
37
37
|
# is the call that finally runs the query. A presenter that builds
|
|
@@ -46,6 +46,136 @@
|
|
|
46
46
|
# `Enumerable` surface, which materialise by delegating to `each`
|
|
47
47
|
# — are covered by the plugin's `effect_attributions:` instead,
|
|
48
48
|
# because declaring them here would change how they TYPE.
|
|
49
|
+
#
|
|
50
|
+
# ## Every bound here also bounds the association's relations
|
|
51
|
+
#
|
|
52
|
+
# A `has_many` reader returns an
|
|
53
|
+
# `ActiveRecord::Associations::CollectionProxy`, and a query
|
|
54
|
+
# builder called on the proxy (`user.posts.where(…)`) returns an
|
|
55
|
+
# `ActiveRecord::AssociationRelation`. Both are Relation subclasses
|
|
56
|
+
# that still hold the association, and the plugin types both as
|
|
57
|
+
# `Relation[Model]`. A call on either imports this file's bound, so
|
|
58
|
+
# each annotation has to hold for all three classes. The facts below
|
|
59
|
+
# are checked against activerecord 8.1.3.1.
|
|
60
|
+
#
|
|
61
|
+
# Where one of them adds records to the association's in-memory
|
|
62
|
+
# target or discards them from it, the bound carries `mutate.self`:
|
|
63
|
+
#
|
|
64
|
+
# - `build` / `new`, `create` / `create!` and the `find_or_*`,
|
|
65
|
+
# `create_or_find_by` and `first_or_*` builders reach
|
|
66
|
+
# `@association.build` / `create`, which call `add_to_target`. On
|
|
67
|
+
# the proxy they are overrides. On an AssociationRelation they are
|
|
68
|
+
# its private `_new` / `_create`.
|
|
69
|
+
# - `reset`, `reload`, `delete_all` and `destroy_all` on the proxy
|
|
70
|
+
# reset the association, which drops every record built there and
|
|
71
|
+
# not yet saved. So do `update_all` and `touch_all`: the proxy does
|
|
72
|
+
# not override them, and Relation's version ends with
|
|
73
|
+
# `.tap { reset }`, which reaches the proxy's `reset`.
|
|
74
|
+
# - `insert`, `insert!` and `upsert`, and their `_all` forms, on the
|
|
75
|
+
# proxy reset the target only when it holds no unsaved record.
|
|
76
|
+
# Rails 8.1 deprecates the other branch and says those records will
|
|
77
|
+
# be lost in 8.2. The bound is written for 8.2, and it over-states
|
|
78
|
+
# 8.1.
|
|
79
|
+
#
|
|
80
|
+
# So `user.posts.build` changes an object the caller can still reach
|
|
81
|
+
# through `user`: `user.posts.size` counts the new record, and
|
|
82
|
+
# `user.save` saves it. On a plain Relation, `new` and `build` change
|
|
83
|
+
# nothing, so `mutate.self` over-states them there. The type cannot
|
|
84
|
+
# tell the receivers apart, and an upper bound may be too wide but
|
|
85
|
+
# never too narrow.
|
|
86
|
+
#
|
|
87
|
+
# Loading is not counted as a change. `to_a` and `load` fill the
|
|
88
|
+
# target with what the database holds, which is the `io.db.read` they
|
|
89
|
+
# already carry.
|
|
90
|
+
#
|
|
91
|
+
# `mutate.self` is written from the callee's side, as any envelope
|
|
92
|
+
# is: the receiver changes itself. A caller imports the label
|
|
93
|
+
# unchanged, so in the caller's row it names the proxy it called,
|
|
94
|
+
# not the caller's own `self`.
|
|
95
|
+
#
|
|
96
|
+
# `insert`, `insert!` and `upsert` are declared here, beside their
|
|
97
|
+
# `_all` forms, because a plain Relation defines them too. The
|
|
98
|
+
# declaration changes no result type: an undeclared method on this
|
|
99
|
+
# open receiver already types as `untyped`. It adds the checks any
|
|
100
|
+
# declared method gets, `call.wrong-arity` and
|
|
101
|
+
# `call.possible-nil-receiver`, and its parameter list admits every
|
|
102
|
+
# call Rails accepts.
|
|
103
|
+
#
|
|
104
|
+
# The writers a plain Relation does not define (`<<`, `push`,
|
|
105
|
+
# `append`, `concat`, `replace`, `clear`) are rows in the plugin's
|
|
106
|
+
# `effect_attributions:` rather than declarations here. The same rows
|
|
107
|
+
# cover `delete` / `destroy`, which a Relation does define, as
|
|
108
|
+
# `delete(id_or_array)` and `destroy(id)`, but the proxy overrides as
|
|
109
|
+
# `delete(*records)` and `destroy(*records)`. A declaration here
|
|
110
|
+
# would need a parameter list and a return wide enough for both, and
|
|
111
|
+
# it would give the two methods a second effect channel next to
|
|
112
|
+
# their rows.
|
|
113
|
+
#
|
|
114
|
+
# ## Every parameter list here also admits the overrides
|
|
115
|
+
#
|
|
116
|
+
# The rule above holds for parameter lists as well as effect bounds.
|
|
117
|
+
# A call on the proxy or on an AssociationRelation is checked against
|
|
118
|
+
# the parameter list declared here, so a declaration must accept
|
|
119
|
+
# every argument an override accepts, or valid Rails draws
|
|
120
|
+
# `call.wrong-arity`. Of the overrides this file declares, only the
|
|
121
|
+
# proxy's `delete_all(dependent = nil)` accepts more than the
|
|
122
|
+
# Relation's `delete_all`. It takes `:nullify` or `:delete_all`, so
|
|
123
|
+
# `delete_all` declares an optional argument. The price is that
|
|
124
|
+
# `delete_all(:nullify)` on a plain Relation or on an
|
|
125
|
+
# AssociationRelation (`user.posts.where(…)`), neither of which
|
|
126
|
+
# overrides it, raises `ArgumentError` and goes unreported.
|
|
127
|
+
#
|
|
128
|
+
# Of the rest, the `(...)` methods of the `insert` and `insert_all`
|
|
129
|
+
# families and the proxy's `delegate … to: :scope` block (the query
|
|
130
|
+
# builders and `scoping`) forward their arguments unchanged to the
|
|
131
|
+
# Relation's own methods, and the proxy's other overrides (`find`,
|
|
132
|
+
# `last`, `take`, `build`, `create`, `calculate`, `pluck`, …) take no
|
|
133
|
+
# more than the declarations here. `delete` and `destroy`, whose
|
|
134
|
+
# overrides take more arguments than the Relation's, stay undeclared
|
|
135
|
+
# for the reasons above.
|
|
136
|
+
#
|
|
137
|
+
# ## A writer that also queries carries the read too
|
|
138
|
+
#
|
|
139
|
+
# `io.db.write` does not include `io.db.read`: they are sibling
|
|
140
|
+
# leaves. So a writer that queries before its write, or after a failed
|
|
141
|
+
# one, names both. In activerecord 8.1.3.1:
|
|
142
|
+
#
|
|
143
|
+
# - `find_or_create_by(!)` is `find_by || create_or_find_by`, and
|
|
144
|
+
# `first_or_create(!)` is `first || create`.
|
|
145
|
+
# - `create_or_find_by(!)` rescues `RecordNotUnique` with `find_by!`.
|
|
146
|
+
# - `destroy_all` is `records.each(&:destroy)`, which loads first, and
|
|
147
|
+
# `destroy_by` is `where(…).destroy_all`. `update(!)` walks `each`,
|
|
148
|
+
# or passes an id to the model's `update`, which calls `find`.
|
|
149
|
+
# - `update_all` and `delete_all`, and so `touch_all` and `delete_by`,
|
|
150
|
+
# first `SELECT` the distinct primary keys when the relation
|
|
151
|
+
# eager-loads, has a limit or offset and no `group`, and eager-loads
|
|
152
|
+
# or joins a collection association (`apply_join_dependency`). The
|
|
153
|
+
# proxy's `delete_all` on a `has_many :through` loads the target
|
|
154
|
+
# before it deletes.
|
|
155
|
+
# - `create(!)` on the proxy of a `has_many :through` a `has_one`
|
|
156
|
+
# builds the join record through the `has_one`, which loads the
|
|
157
|
+
# record it replaces.
|
|
158
|
+
#
|
|
159
|
+
# The `insert` and `insert_all` families stay `io.db.write` alone.
|
|
160
|
+
# Each compiles one `INSERT` statement and queries nothing else: the
|
|
161
|
+
# proxy's override reads only the target it already holds in memory.
|
|
162
|
+
#
|
|
163
|
+
# Two kinds of query are not counted. Schema reflection is one: the
|
|
164
|
+
# column and index lookups the schema cache serves would otherwise
|
|
165
|
+
# make `where` a read. The model's callbacks and validators are the
|
|
166
|
+
# other, including those an association option such as `dependent:`
|
|
167
|
+
# or `touch:` registers. They are not part of any bound here. The
|
|
168
|
+
# plugin's callback edge carries only symbol-argument callback macros
|
|
169
|
+
# and a uniqueness validator, and only on the model's own `save` /
|
|
170
|
+
# `destroy`. Nothing carries the reads a required `belongs_to`,
|
|
171
|
+
# `touch:` or `dependent:` registers.
|
|
172
|
+
#
|
|
173
|
+
# One case is not covered. On that same `has_many :through` a
|
|
174
|
+
# `has_one`, `new` / `build` also build the join record when the
|
|
175
|
+
# source reflection has an inverse, and so do `find_or_initialize_by`
|
|
176
|
+
# and `first_or_initialize`, which reach `new`. The `has_one` then
|
|
177
|
+
# replaces its record: it loads the old one, and even without a
|
|
178
|
+
# save it can delete, destroy or save it.
|
|
49
179
|
|
|
50
180
|
module ActiveRecord
|
|
51
181
|
class Relation[Elem]
|
|
@@ -101,6 +231,10 @@ module ActiveRecord
|
|
|
101
231
|
def left_outer_joins: (*untyped) -> self
|
|
102
232
|
%a{pure}
|
|
103
233
|
def references: (*untyped) -> self
|
|
234
|
+
# With a block, `select` is Enumerable's and returns an Array. It
|
|
235
|
+
# stays `self`: on this open receiver a method the file does not
|
|
236
|
+
# declare is not reported, while on `Array[Elem]` any ActiveSupport
|
|
237
|
+
# method the bundled overlay leaves out would be.
|
|
104
238
|
%a{pure}
|
|
105
239
|
def select: (*untyped) -> self
|
|
106
240
|
%a{pure}
|
|
@@ -145,9 +279,33 @@ module ActiveRecord
|
|
|
145
279
|
def with: (*untyped) -> self
|
|
146
280
|
def scoping: () { () -> untyped } -> untyped
|
|
147
281
|
|
|
148
|
-
# --- finders (return an element
|
|
149
|
-
|
|
150
|
-
|
|
282
|
+
# --- finders (return an element, or an Array of them; `find`
|
|
283
|
+
# with a block, the element or nil) ---
|
|
284
|
+
# Two or more ids return an Array, unless dropping `nil`s and
|
|
285
|
+
# duplicates leaves one. One argument returns an Array when it is an
|
|
286
|
+
# Array of ids, but one record when it is a composite key's tuple
|
|
287
|
+
# (`[shop_id, id]`), and nothing here can tell those apart. An
|
|
288
|
+
# overload keyed on an `Array` argument would be selected for every
|
|
289
|
+
# untyped id too (`find(params[:id])`). Its return would be joined
|
|
290
|
+
# with this one's into `Dynamic[Array[Elem] | Elem]`, on which a
|
|
291
|
+
# following `update` or `save` no longer resolves on the model and
|
|
292
|
+
# loses its `io.db.write`. So a single argument stays `Elem`. A
|
|
293
|
+
# splat and a `**` hash each count as one argument, so `find(*ids)`
|
|
294
|
+
# is `Elem`, while `find(id, *ids)` and `find(*args, **opts)` are
|
|
295
|
+
# Arrays even when the splat or the hash turns out empty.
|
|
296
|
+
#
|
|
297
|
+
# With a block `find` is `Enumerable#find` (`return super if
|
|
298
|
+
# block_given?`), so the id overloads do not apply: no argument
|
|
299
|
+
# returns the element or `nil`, and one argument is the `ifnone`
|
|
300
|
+
# callable, whose result joins the union. That form is rare, so
|
|
301
|
+
# it stays `untyped` rather than typing the callable. The
|
|
302
|
+
# class-side `Model.find { … }` reaches this same method through
|
|
303
|
+
# `all`.
|
|
304
|
+
%a{rigor:v1:effect io.db.read}
|
|
305
|
+
def find: () { (Elem) -> boolish } -> Elem?
|
|
306
|
+
| (untyped ifnone) { (Elem) -> boolish } -> untyped
|
|
307
|
+
| (untyped, untyped, *untyped) -> Array[Elem]
|
|
308
|
+
| (?untyped id) -> Elem
|
|
151
309
|
%a{rigor:v1:effect io.db.read}
|
|
152
310
|
def find_by: (*untyped) -> Elem?
|
|
153
311
|
%a{rigor:v1:effect io.db.read}
|
|
@@ -243,9 +401,9 @@ module ActiveRecord
|
|
|
243
401
|
def entries: () -> Array[Elem]
|
|
244
402
|
%a{rigor:v1:effect io.db.read}
|
|
245
403
|
def load: (?untyped) -> self
|
|
246
|
-
%a{rigor:v1:effect io.db.read}
|
|
404
|
+
%a{rigor:v1:effect io.db.read, mutate.self}
|
|
247
405
|
def reload: () -> self
|
|
248
|
-
%a{
|
|
406
|
+
%a{rigor:v1:effect mutate.self}
|
|
249
407
|
def reset: () -> self
|
|
250
408
|
%a{pure}
|
|
251
409
|
def loaded?: () -> bool
|
|
@@ -272,56 +430,73 @@ module ActiveRecord
|
|
|
272
430
|
%a{pure}
|
|
273
431
|
def table: () -> untyped
|
|
274
432
|
%a{pure}
|
|
275
|
-
def arel: () -> untyped
|
|
433
|
+
def arel: (?untyped aliases) -> untyped
|
|
276
434
|
|
|
277
435
|
# --- whole-relation persistence ---
|
|
278
|
-
%a{rigor:v1:effect io.db.write}
|
|
436
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
279
437
|
def update_all: (untyped) -> Integer
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
438
|
+
# The optional argument is the association proxy's `dependent`
|
|
439
|
+
# (see the header). A plain Relation's and an AssociationRelation's
|
|
440
|
+
# `delete_all` take none.
|
|
441
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
442
|
+
def delete_all: (?untyped dependent) -> Integer
|
|
443
|
+
# On a plain `has_many`, the proxy's `destroy_all` returns `nil`
|
|
444
|
+
# when a `before_remove` callback throws `:abort`. A `:through` one,
|
|
445
|
+
# which includes `has_and_belongs_to_many`, still returns the
|
|
446
|
+
# records. `Array[Elem]?` would report `call.possible-nil-receiver`
|
|
447
|
+
# on every `destroyed.each` for a case only such a callback reaches.
|
|
448
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
283
449
|
def destroy_all: () -> Array[Elem]
|
|
284
|
-
%a{rigor:v1:effect io.db.write}
|
|
450
|
+
%a{rigor:v1:effect io.db.read, io.db.write}
|
|
285
451
|
def update: (*untyped) -> untyped
|
|
286
|
-
%a{rigor:v1:effect io.db.write}
|
|
452
|
+
%a{rigor:v1:effect io.db.read, io.db.write}
|
|
287
453
|
def update!: (*untyped) -> untyped
|
|
288
|
-
%a{rigor:v1:effect io.db.write}
|
|
454
|
+
%a{rigor:v1:effect io.db.read, io.db.write}
|
|
289
455
|
def delete_by: (*untyped) -> Integer
|
|
290
|
-
%a{rigor:v1:effect io.db.write}
|
|
456
|
+
%a{rigor:v1:effect io.db.read, io.db.write}
|
|
291
457
|
def destroy_by: (*untyped) -> Array[Elem]
|
|
292
|
-
%a{rigor:v1:effect io.db.write}
|
|
458
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
293
459
|
def touch_all: (*untyped) -> Integer
|
|
294
|
-
%a{rigor:v1:effect io.db.write}
|
|
460
|
+
%a{rigor:v1:effect io.db.write, mutate.self}
|
|
461
|
+
def insert: (untyped, **untyped) -> untyped
|
|
462
|
+
%a{rigor:v1:effect io.db.write, mutate.self}
|
|
295
463
|
def insert_all: (untyped, **untyped) -> untyped
|
|
296
|
-
%a{rigor:v1:effect io.db.write}
|
|
464
|
+
%a{rigor:v1:effect io.db.write, mutate.self}
|
|
465
|
+
def insert!: (untyped, **untyped) -> untyped
|
|
466
|
+
%a{rigor:v1:effect io.db.write, mutate.self}
|
|
297
467
|
def insert_all!: (untyped, **untyped) -> untyped
|
|
298
|
-
%a{rigor:v1:effect io.db.write}
|
|
468
|
+
%a{rigor:v1:effect io.db.write, mutate.self}
|
|
469
|
+
def upsert: (untyped, **untyped) -> untyped
|
|
470
|
+
%a{rigor:v1:effect io.db.write, mutate.self}
|
|
299
471
|
def upsert_all: (untyped, **untyped) -> untyped
|
|
300
472
|
|
|
301
473
|
# --- instantiating builders (return an element) ---
|
|
302
|
-
|
|
474
|
+
# Given an Array of attribute hashes, `new`, `build`, `create` and
|
|
475
|
+
# `create!` return an Array of records. No overload says so, for
|
|
476
|
+
# the reason `find` gives: an untyped argument would select it too.
|
|
477
|
+
%a{rigor:v1:effect mutate.self}
|
|
303
478
|
def new: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
304
|
-
%a{
|
|
479
|
+
%a{rigor:v1:effect mutate.self}
|
|
305
480
|
def build: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
306
|
-
%a{rigor:v1:effect io.db.write}
|
|
481
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
307
482
|
def create: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
308
|
-
%a{rigor:v1:effect io.db.write}
|
|
483
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
309
484
|
def create!: (*untyped) ?{ (Elem) -> void } -> Elem
|
|
310
|
-
%a{rigor:v1:effect io.db.write}
|
|
485
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
311
486
|
def find_or_create_by: (untyped) ?{ (Elem) -> void } -> Elem
|
|
312
|
-
%a{rigor:v1:effect io.db.write}
|
|
487
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
313
488
|
def find_or_create_by!: (untyped) ?{ (Elem) -> void } -> Elem
|
|
314
|
-
%a{rigor:v1:effect io.db.read}
|
|
489
|
+
%a{rigor:v1:effect io.db.read, mutate.self}
|
|
315
490
|
def find_or_initialize_by: (untyped) ?{ (Elem) -> void } -> Elem
|
|
316
|
-
%a{rigor:v1:effect io.db.write}
|
|
491
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
317
492
|
def create_or_find_by: (untyped) ?{ (Elem) -> void } -> Elem
|
|
318
|
-
%a{rigor:v1:effect io.db.write}
|
|
493
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
319
494
|
def create_or_find_by!: (untyped) ?{ (Elem) -> void } -> Elem
|
|
320
|
-
%a{rigor:v1:effect io.db.write}
|
|
495
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
321
496
|
def first_or_create: (?untyped) ?{ (Elem) -> void } -> Elem
|
|
322
|
-
%a{rigor:v1:effect io.db.write}
|
|
497
|
+
%a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
|
|
323
498
|
def first_or_create!: (?untyped) ?{ (Elem) -> void } -> Elem
|
|
324
|
-
%a{rigor:v1:effect io.db.read}
|
|
499
|
+
%a{rigor:v1:effect io.db.read, mutate.self}
|
|
325
500
|
def first_or_initialize: (?untyped) ?{ (Elem) -> void } -> Elem
|
|
326
501
|
end
|
|
327
502
|
end
|
|
@@ -67,12 +67,15 @@ module Rigor
|
|
|
67
67
|
def init(_services)
|
|
68
68
|
@model_search_paths = Array(config.fetch("model_search_paths")).map(&:to_s)
|
|
69
69
|
@attachment_index = nil
|
|
70
|
-
@load_errors =
|
|
70
|
+
@load_errors = {}
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
74
74
|
index = attachment_index
|
|
75
|
-
|
|
75
|
+
if index.nil?
|
|
76
|
+
disclose_load_errors
|
|
77
|
+
return []
|
|
78
|
+
end
|
|
76
79
|
return [] if index.empty?
|
|
77
80
|
|
|
78
81
|
Analyzer.new(path: path, attachment_index: index).analyze(root).diagnostics
|
|
@@ -126,24 +129,32 @@ module Rigor
|
|
|
126
129
|
# covers model-file additions.
|
|
127
130
|
@attachment_index = cache_for(:attachment_index, params: {}).call
|
|
128
131
|
rescue Plugin::AccessDeniedError => e
|
|
129
|
-
@load_errors
|
|
132
|
+
@load_errors["1-read-refused"] ||= "rigor-activestorage: #{e.message}"
|
|
130
133
|
nil
|
|
131
134
|
rescue StandardError => e
|
|
132
|
-
@load_errors
|
|
135
|
+
@load_errors["2-discovery-failed"] ||=
|
|
136
|
+
"rigor-activestorage: discovery failed: #{e.class}: #{e.message}"
|
|
133
137
|
nil
|
|
134
138
|
end
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
# Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not about the
|
|
141
|
+
# file being analysed, so both outcomes are registered rather than returned: see
|
|
142
|
+
# {Plugin::Base#disclose_once} for the channel and the position. Returned from the per-file hook they
|
|
143
|
+
# carried no once-guard at all — the `.uniq` collapsed repeats within one instance's list, but the
|
|
144
|
+
# whole list was re-emitted on every analysed file.
|
|
145
|
+
#
|
|
146
|
+
# The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
|
|
147
|
+
# (#1051), and a refused read must still precede a discovery failure the way `@load_errors` records
|
|
148
|
+
# them — `@load_errors` is now keyed by that same string, which also caps it: `#attachment_index`
|
|
149
|
+
# re-attempts the load per call site, and the old Array appended a fresh interpolated string every
|
|
150
|
+
# time (the unbounded-growth shape #569 measured at 4.2 M retained strings on Redmine). The key, not
|
|
151
|
+
# the message, is the identity: a second refusal naming a different path collapses into the first row
|
|
152
|
+
# rather than adding one — the notice is "the index did not load", said once.
|
|
153
|
+
def disclose_load_errors
|
|
154
|
+
@load_errors.each do |key, message|
|
|
155
|
+
disclose_once(key, message: message, severity: :warning, rule: "load-error")
|
|
146
156
|
end
|
|
157
|
+
nil
|
|
147
158
|
end
|
|
148
159
|
end
|
|
149
160
|
|
data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Rigor
|
|
|
8
8
|
# rigor-activesupport-core-ext's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
|
|
9
9
|
#
|
|
10
10
|
# **Scope note.** This is the *impure* half of ActiveSupport only: the clock, the notification bus and
|
|
11
|
-
# `CurrentAttributes`. The `%a{pure}` sweep over `blank?` / `present?` / `
|
|
11
|
+
# `CurrentAttributes`. The `%a{pure}` sweep over `blank?` / `present?` / `presence` and the rest of
|
|
12
12
|
# the core_ext predicate surface — the single cheapest purity win in a Rails app, per WD10 — is issue
|
|
13
13
|
# #388 and lands in this plugin's shipped RBS, not here (`try` was audited and skipped: it dispatches
|
|
14
14
|
# to whatever method the caller names, so its purity is a fact about the call site, not about `try`).
|
|
@@ -115,11 +115,18 @@ module Rigor
|
|
|
115
115
|
# which is what earns `Date` its row here — the label is a fact about the module, not about the
|
|
116
116
|
# receiver, and the three only ever differed in which spellings the bundle had got round to
|
|
117
117
|
# declaring.
|
|
118
|
+
#
|
|
119
|
+
# `String#in_time_zone` is the exception: it has no instant to convert. It parses through
|
|
120
|
+
# `TimeZone#parse(str, now = now())`, whose default argument reads the clock on every call, or through
|
|
121
|
+
# `to_time` when no zone is set — so it takes `CLOCK`, not `ZONE_READ`.
|
|
118
122
|
def in_time_zone_rows
|
|
119
123
|
[TIME, DATE, DATETIME].map do |receiver|
|
|
120
124
|
row(receiver, :in_time_zone, ZONE_READ,
|
|
121
125
|
why: "the default argument reads `Time.zone` when the caller doesn't name one explicitly")
|
|
122
|
-
end
|
|
126
|
+
end + [
|
|
127
|
+
row("String", :in_time_zone, CLOCK,
|
|
128
|
+
why: "parses in `Time.zone` (the default argument) and fills the parts from the zone's `now`")
|
|
129
|
+
]
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
def current_rows
|
|
@@ -163,6 +163,10 @@ class Object
|
|
|
163
163
|
# exception to the try-shaped-dispatch rule above (design note § 11.2).
|
|
164
164
|
%a{pure}
|
|
165
165
|
def in?: (untyped) -> bool
|
|
166
|
+
# `in?(another_object) ? self : nil` — `params[:kind].presence_in(%w[a b])`.
|
|
167
|
+
# The same `#include?` dispatch as `in?`, and the same exception.
|
|
168
|
+
%a{pure}
|
|
169
|
+
def presence_in: (untyped) -> self?
|
|
166
170
|
|
|
167
171
|
# Issue #673 — `core_ext/object/to_query`, `core_ext/object/duplicable` and
|
|
168
172
|
# `core_ext/object/instance_variables` all hang off `Object`, so every
|
|
@@ -191,6 +195,63 @@ class Object
|
|
|
191
195
|
# `instance_variable_names`.
|
|
192
196
|
def instance_values: () -> Hash[String, untyped]
|
|
193
197
|
def instance_variable_names: () -> Array[String]
|
|
198
|
+
|
|
199
|
+
# `core_ext/object/deep_dup` — `duplicable? ? dup : self`, plus `Array`
|
|
200
|
+
# (`map(&:deep_dup)`), `Hash` and `Module` overrides. `self` over-claims on
|
|
201
|
+
# an `Array` subclass, whose override answers a plain `Array`. Declared on
|
|
202
|
+
# `Object` and not only on `Hash`, where it sat alone: `[[1], [2]].deep_dup`
|
|
203
|
+
# is the common shape.
|
|
204
|
+
#
|
|
205
|
+
# NOT `%a{pure}`: on `Object` the `dup` runs the receiver class's own
|
|
206
|
+
# `initialize_copy`, an unknown-at-this-declaration dispatch like the one
|
|
207
|
+
# that keeps `as_json` bare. `Hash#deep_dup` below reaches the same `dup`
|
|
208
|
+
# through `value.deep_dup` on every value and is bare for the same reason.
|
|
209
|
+
def deep_dup: () -> self
|
|
210
|
+
|
|
211
|
+
# `core_ext/object/with` (ActiveSupport 7.1+) — sets each keyword through
|
|
212
|
+
# its public writer, yields `self`, and restores the old values in an
|
|
213
|
+
# `ensure`; the answer is the block's. There is no blockless form: `yield` without a block raises.
|
|
214
|
+
# ActiveSupport `undef_method`s it on `NilClass`, `TrueClass`, `FalseClass`,
|
|
215
|
+
# `Integer`, `Float` and `Symbol`, which RBS cannot express, so `1.with(a: 1)`
|
|
216
|
+
# resolves here where Ruby raises — a missed error, not a false one.
|
|
217
|
+
# `Data#with` (core) is a subclass override and keeps its own `-> self`.
|
|
218
|
+
#
|
|
219
|
+
# NOT `%a{pure}`: it writes the receiver's attributes through `public_send`.
|
|
220
|
+
def with: [T] (**untyped attributes) { (self) -> T } -> T
|
|
221
|
+
|
|
222
|
+
# `core_ext/object/with_options` — without a block, the
|
|
223
|
+
# `ActiveSupport::OptionMerger` proxy (not declared here); with one, the
|
|
224
|
+
# block's value. A zero-arity block is `instance_eval`ed on the proxy, the
|
|
225
|
+
# `with_options dependent: :destroy do has_many … end` model-body shape.
|
|
226
|
+
#
|
|
227
|
+
# NOT `%a{pure}`: the proxy forwards every call to the receiver through
|
|
228
|
+
# `method_missing`, a dispatch this audit cannot see.
|
|
229
|
+
def with_options: (untyped options) -> untyped
|
|
230
|
+
| [T] (untyped options) { (?) -> T } -> T
|
|
231
|
+
|
|
232
|
+
# `core_ext/string/output_safety` — `false` on `Object`, `true` on
|
|
233
|
+
# `Numeric`, the buffer's own state on `ActiveSupport::SafeBuffer`. `bool`
|
|
234
|
+
# for the reason `duplicable?` is: `-> false` on the universal receiver makes
|
|
235
|
+
# every `if x.html_safe?` an always-falsy branch on the subclasses that answer
|
|
236
|
+
# true. All three bodies are a constant or an ivar read.
|
|
237
|
+
%a{pure}
|
|
238
|
+
def html_safe?: () -> bool
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------
|
|
242
|
+
# Kernel — `core_ext/kernel/singleton_class`
|
|
243
|
+
# ---------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
# `singleton_class.class_eval(*args, &block)` on any object. Declared on
|
|
246
|
+
# `Kernel`, where ActiveSupport defines it; a `Module` receiver still answers
|
|
247
|
+
# core `Module#class_eval`, which is its own alias of `module_eval`. The two
|
|
248
|
+
# overloads mirror that `module_eval`: a string, or a block given the
|
|
249
|
+
# singleton class.
|
|
250
|
+
module Kernel
|
|
251
|
+
# NOT `%a{pure}`: it evaluates a string or a block in the singleton class,
|
|
252
|
+
# and a block that defines methods is the point.
|
|
253
|
+
def class_eval: (string code, ?string? filename, ?int lineno) -> untyped
|
|
254
|
+
| [U] () { (untyped singleton_class) -> U } -> U
|
|
194
255
|
end
|
|
195
256
|
|
|
196
257
|
# `nil.blank?` / `nil.present?` / `nil.try` are the most frequent
|
|
@@ -269,6 +330,8 @@ class String
|
|
|
269
330
|
def dasherize: () -> String
|
|
270
331
|
%a{pure}
|
|
271
332
|
def upcase_first: () -> String
|
|
333
|
+
%a{pure}
|
|
334
|
+
def downcase_first: () -> String # ActiveSupport 7.1+
|
|
272
335
|
# NOT %a{pure}: transliterates through `I18n.transliterate`, which reads
|
|
273
336
|
# `I18n.locale` when `locale:` is left at its `nil` default — a request-
|
|
274
337
|
# scoped global read the inflection-table assumption does not cover.
|
|
@@ -284,6 +347,11 @@ class String
|
|
|
284
347
|
%a{pure}
|
|
285
348
|
def humanize: (?capitalize: bool, ?keep_id_suffix: bool) -> String
|
|
286
349
|
|
|
350
|
+
# `core_ext/string/behavior` — the `acts_like?(:string)` duck-type hook, a
|
|
351
|
+
# hardcoded `true`.
|
|
352
|
+
%a{pure}
|
|
353
|
+
def acts_like_string?: () -> true
|
|
354
|
+
|
|
287
355
|
# `core_ext/string/filters`
|
|
288
356
|
%a{pure}
|
|
289
357
|
def squish: () -> String
|
|
@@ -336,6 +404,15 @@ class String
|
|
|
336
404
|
# follow-up to confirm and either implement or remove the declaration.
|
|
337
405
|
def to_hours: () -> Float
|
|
338
406
|
|
|
407
|
+
# `core_ext/string/zones` — `Time.find_zone!(zone).parse(self)`, or
|
|
408
|
+
# `to_time` when no zone is set. `untyped` like the `Time` / `Date` rows:
|
|
409
|
+
# the parse answers an `ActiveSupport::TimeWithZone`, or `nil` on a string
|
|
410
|
+
# with no date parts. NOT `%a{pure}`: the default argument reads
|
|
411
|
+
# `Time.zone`, and `TimeZone#parse(str, now = now())` reads the clock on
|
|
412
|
+
# every call, whether or not the string names every part. The plugin's
|
|
413
|
+
# `effect_attributions:` carry both labels.
|
|
414
|
+
def in_time_zone: (?untyped zone) -> untyped
|
|
415
|
+
|
|
339
416
|
# `core_ext/string/access`
|
|
340
417
|
%a{pure}
|
|
341
418
|
def at: (Integer | Range[Integer] | Regexp position) -> String?
|
|
@@ -355,12 +432,29 @@ class String
|
|
|
355
432
|
# `core_ext/string/multibyte`
|
|
356
433
|
%a{pure}
|
|
357
434
|
def mb_chars: () -> untyped
|
|
435
|
+
# Reads `encoding` / `valid_encoding?`; the `ASCII-8BIT` branch
|
|
436
|
+
# `force_encoding`s a `dup`, never the receiver.
|
|
437
|
+
%a{pure}
|
|
438
|
+
def is_utf8?: () -> bool
|
|
358
439
|
|
|
359
440
|
# `core_ext/string/inquiry`
|
|
360
441
|
%a{pure}
|
|
361
442
|
def inquiry: () -> untyped # ActiveSupport::StringInquirer
|
|
362
443
|
end
|
|
363
444
|
|
|
445
|
+
# ---------------------------------------------------------------
|
|
446
|
+
# Symbol — `core_ext/symbol/starts_ends_with`
|
|
447
|
+
# ---------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
# `alias_method`s of core `start_with?` / `end_with?`, declared with the core
|
|
450
|
+
# parameter lists.
|
|
451
|
+
class Symbol
|
|
452
|
+
%a{pure}
|
|
453
|
+
def starts_with?: (*Regexp | string prefixes) -> bool
|
|
454
|
+
%a{pure}
|
|
455
|
+
def ends_with?: (*string suffixes) -> bool
|
|
456
|
+
end
|
|
457
|
+
|
|
364
458
|
# ---------------------------------------------------------------
|
|
365
459
|
# Integer — `core_ext/integer/*`
|
|
366
460
|
# ---------------------------------------------------------------
|
|
@@ -1369,6 +1463,10 @@ class Array[unchecked out Elem]
|
|
|
1369
1463
|
def fifth: () -> Elem?
|
|
1370
1464
|
%a{pure}
|
|
1371
1465
|
def forty_two: () -> Elem?
|
|
1466
|
+
%a{pure}
|
|
1467
|
+
def second_to_last: () -> Elem?
|
|
1468
|
+
%a{pure}
|
|
1469
|
+
def third_to_last: () -> Elem?
|
|
1372
1470
|
|
|
1373
1471
|
# `core_ext/array/grouping`
|
|
1374
1472
|
%a{pure}
|
|
@@ -1400,6 +1498,9 @@ class Array[unchecked out Elem]
|
|
|
1400
1498
|
# `core_ext/array/extract`
|
|
1401
1499
|
def extract!: () { (Elem) -> bool } -> Array[Elem] # NOT %a{pure}: bang, mutates in place.
|
|
1402
1500
|
|
|
1501
|
+
# `core_ext/array/extract_options`
|
|
1502
|
+
def extract_options!: () -> Hash[untyped, untyped] # NOT %a{pure}: pops the trailing options Hash.
|
|
1503
|
+
|
|
1403
1504
|
# `core_ext/object/blank` / `core_ext/enumerable` — both
|
|
1404
1505
|
# ActiveSupport additions, shipped on Array specifically.
|
|
1405
1506
|
%a{pure}
|
|
@@ -1444,6 +1545,15 @@ module Enumerable[unchecked out Elem]
|
|
|
1444
1545
|
def sole: () -> Elem
|
|
1445
1546
|
%a{pure}
|
|
1446
1547
|
def compact_blank: () -> Array[Elem]
|
|
1548
|
+
%a{pure}
|
|
1549
|
+
def many?: () -> bool
|
|
1550
|
+
| () { (Elem) -> boolish } -> bool
|
|
1551
|
+
# `key` and `series` are untyped on purpose: the default `filter: true`
|
|
1552
|
+
# path is `group_by(&key).values_at(*series)`, so a Proc key or a Set /
|
|
1553
|
+
# Range series is correct Rails code. `filter:` is ActiveSupport 8.x;
|
|
1554
|
+
# passing it on 7.x raises, which this unversioned row does not catch.
|
|
1555
|
+
%a{pure}
|
|
1556
|
+
def in_order_of: (untyped key, untyped series, ?filter: boolish) -> Array[Elem]
|
|
1447
1557
|
end
|
|
1448
1558
|
|
|
1449
1559
|
# ---------------------------------------------------------------
|
|
@@ -1474,10 +1584,17 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
1474
1584
|
%a{pure}
|
|
1475
1585
|
def deep_transform_values: () { (V) -> untyped } -> Hash[K, untyped]
|
|
1476
1586
|
def deep_transform_values!: () { (V) -> untyped } -> self
|
|
1587
|
+
# `alias_method`s of `symbolize_keys` / `symbolize_keys!`.
|
|
1588
|
+
%a{pure}
|
|
1589
|
+
def to_options: () -> Hash[Symbol, V]
|
|
1590
|
+
def to_options!: () -> self
|
|
1477
1591
|
|
|
1478
1592
|
# `core_ext/hash/deep_dup` — allocates a deep copy, never touches the
|
|
1479
|
-
# receiver.
|
|
1480
|
-
|
|
1593
|
+
# receiver. NOT `%a{pure}`: it calls `deep_dup` on every value, and on
|
|
1594
|
+
# every key that is not a `String` or `Symbol` — an ELEMENT of unknown class
|
|
1595
|
+
# whose `dup` runs that class's own `initialize_copy`. That is the dispatch
|
|
1596
|
+
# the purity rule at the top of this file excludes, and the reason
|
|
1597
|
+
# `Object#deep_dup` above is bare.
|
|
1481
1598
|
def deep_dup: () -> Hash[K, V]
|
|
1482
1599
|
|
|
1483
1600
|
# `core_ext/hash/deep_merge` — the block receives `(key, this_val,
|
|
@@ -1488,6 +1605,10 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
1488
1605
|
| (Hash[K, V]) { (K, V, V) -> V } -> Hash[K, V]
|
|
1489
1606
|
def deep_merge!: (Hash[K, V]) -> self
|
|
1490
1607
|
| (Hash[K, V]) { (K, V, V) -> V } -> self
|
|
1608
|
+
# `other.is_a?(Hash)` — the hook `ActiveSupport::DeepMergeable` asks before
|
|
1609
|
+
# recursing into a value. `:nodoc:`, but public.
|
|
1610
|
+
%a{pure}
|
|
1611
|
+
def deep_merge?: (untyped other) -> bool
|
|
1491
1612
|
|
|
1492
1613
|
# `core_ext/hash/except` — `Hash#except` is in core RBS as of
|
|
1493
1614
|
# Ruby 3.0+; `except!` is ActiveSupport-only. ActiveSupport
|
|
@@ -1512,6 +1633,9 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
1512
1633
|
# `%a{pure}` explicitly.
|
|
1513
1634
|
%a{pure}
|
|
1514
1635
|
def with_indifferent_access: () -> untyped
|
|
1636
|
+
# An `alias` of `with_indifferent_access`.
|
|
1637
|
+
%a{pure}
|
|
1638
|
+
def nested_under_indifferent_access: () -> untyped
|
|
1515
1639
|
|
|
1516
1640
|
# `core_ext/hash/conversions`
|
|
1517
1641
|
def self.from_xml: (String, ?Symbol disallowed_types) -> Hash[String, untyped]
|
|
@@ -1522,15 +1646,57 @@ class Hash[unchecked out K, unchecked out V]
|
|
|
1522
1646
|
def compact_blank: () -> Hash[K, V]
|
|
1523
1647
|
def compact_blank!: () -> self # NOT %a{pure}: bang (`delete_if`), mutates in place.
|
|
1524
1648
|
|
|
1525
|
-
# `core_ext/
|
|
1649
|
+
# `core_ext/array/extract_options`
|
|
1526
1650
|
%a{pure}
|
|
1527
|
-
def
|
|
1651
|
+
def extractable_options?: () -> bool
|
|
1652
|
+
|
|
1653
|
+
# `core_ext/hash/reverse_merge` — `other_hash.merge(self)`, so the result
|
|
1654
|
+
# carries the argument's keys and values as well as the receiver's, the
|
|
1655
|
+
# same shape as core `Hash#merge`. `-> Hash[K, V]` typed
|
|
1656
|
+
# `opts.reverse_merge(size: 25, velocity: 10)` as `Hash[:size, 1]`.
|
|
1657
|
+
%a{pure}
|
|
1658
|
+
def reverse_merge: [A, B] (Hash[A, B] other_hash) -> Hash[A | K, B | V]
|
|
1528
1659
|
def reverse_merge!: (Hash[K, V]) -> self # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
1660
|
+
# `alias_method`s: `with_defaults` of `reverse_merge`, `with_defaults!`
|
|
1661
|
+
# and `reverse_update` of `reverse_merge!`.
|
|
1662
|
+
%a{pure}
|
|
1663
|
+
def with_defaults: [A, B] (Hash[A, B] other_hash) -> Hash[A | K, B | V]
|
|
1664
|
+
def with_defaults!: (Hash[K, V]) -> self # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
1665
|
+
def reverse_update: (Hash[K, V]) -> self # NOT %a{pure}: `reverse_merge!` under another name.
|
|
1529
1666
|
|
|
1530
1667
|
# `core_ext/hash/slice` — `Hash#except` is in core RBS (Ruby 3.0+);
|
|
1531
1668
|
# `Hash#slice` is in core RBS (Ruby 2.5+); the bang variants are
|
|
1532
|
-
# ActiveSupport-only.
|
|
1669
|
+
# ActiveSupport-only. `extract!` deletes the named keys and answers them
|
|
1670
|
+
# as a new hash; a key the receiver lacks is skipped, not `nil`-filled.
|
|
1533
1671
|
def slice!: (*K) -> Hash[K, V] # NOT %a{pure}: bang (`replace`), mutates in place.
|
|
1672
|
+
def extract!: (*K) -> Hash[K, V] # NOT %a{pure}: bang, mutates via `delete`.
|
|
1673
|
+
end
|
|
1674
|
+
|
|
1675
|
+
# ---------------------------------------------------------------
|
|
1676
|
+
# Range — `core_ext/range/*`
|
|
1677
|
+
# ---------------------------------------------------------------
|
|
1678
|
+
|
|
1679
|
+
# `[out E]` matches core `range.rbs`. A reopening whose parameters differ in
|
|
1680
|
+
# count or variance (`class Range`, `class Range[E]`) raises
|
|
1681
|
+
# `RBS::GenericParameterMismatchError` and collapses `Range`.
|
|
1682
|
+
class Range[out E]
|
|
1683
|
+
# `core_ext/range/overlap` — an `alias` of `overlap?`, which rbs declares
|
|
1684
|
+
# since Ruby 3.3 (ActiveSupport defines the method itself only on an older
|
|
1685
|
+
# Ruby). `overlap?` is therefore NOT redeclared here: a second declaration
|
|
1686
|
+
# is a `DuplicatedMethodDefinitionError` that collapses the whole class.
|
|
1687
|
+
%a{pure}
|
|
1688
|
+
def overlaps?: (Range[untyped]) -> bool
|
|
1689
|
+
|
|
1690
|
+
# `core_ext/range/conversions` — `ActiveSupport::RangeWithFormat`, prepended
|
|
1691
|
+
# onto `Range`. An unknown format falls back to `to_s`; `:db` answers `nil`
|
|
1692
|
+
# for a beginless-and-endless range, which the declared `String` does not
|
|
1693
|
+
# model — `(nil..nil).to_fs(:db)` is not a shape correct code writes.
|
|
1694
|
+
#
|
|
1695
|
+
# NOT `%a{pure}`, for the `Array#to_fs` reasons: `RANGE_FORMATS` is a
|
|
1696
|
+
# mutable constant an initializer extends, and `:db` calls `to_fs(:db)` on
|
|
1697
|
+
# both endpoints.
|
|
1698
|
+
def to_fs: (?Symbol format) -> String
|
|
1699
|
+
def to_formatted_s: (?Symbol format) -> String
|
|
1534
1700
|
end
|
|
1535
1701
|
|
|
1536
1702
|
# ---------------------------------------------------------------
|