rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -67,7 +67,7 @@ carries no `documentation_url`.
|
|
|
67
67
|
| <a id="rule-flow-return-in-ensure"></a>`flow.return-in-ensure` | An explicit `return` inside an `ensure` clause — it overrides the method's in-flight return value and silently swallows any in-flight exception. A `return` in a nested `def`, lambda, or `define_method` block inside the `ensure` does not fire (it exits that inner frame). | high |
|
|
68
68
|
| <a id="rule-flow-shadowed-rescue-clause"></a>`flow.shadowed-rescue-clause` | A `rescue` clause can never run because an earlier clause of the same chain already catches a superclass (or the same class) of every exception class it names. | high |
|
|
69
69
|
| <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
|
|
70
|
-
| <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
|
|
70
|
+
| <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. A write in a block whose `self` is provably another object (`Other.new.instance_eval { @x = 1 }`, `Class.new { … }`, `X.class_eval { … }`, a `class << obj` body) sets that object's variable and is not compared. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
|
|
71
71
|
| <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
|
|
72
72
|
| <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
|
|
73
73
|
| <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
|
|
@@ -75,9 +75,10 @@ carries no `documentation_url`.
|
|
|
75
75
|
| <a id="rule-global-write-type-mismatch"></a>`global.write-type-mismatch` | A special global is assigned a literal its setter rejects, so the write raises `TypeError` every time it runs. `$/`, `$-0`, `$,` and `$\` take only a String or nil (`$/ = 1` and `$/ = /x/` report). `$;` and `$-F` take a String, a Regexp, nil, or an object with `to_str`. `$~` takes a MatchData or nil. `$0` and `$PROGRAM_NAME` take a String or an object with `to_str`, so `$0 = nil` and `$0 = :name` report. `$.` takes an Integer, a Float, or an object with `to_int`, so `$. = "3"` reports and `$. = 3r` does not. `$-i` takes a String, nil, false, or an object with `to_str`. `$stdout`, `$>` and `$stderr` take anything that responds to `write`: `$stdout = 1` reports, `$stdout = StringIO.new` does not. Ruby's setter decides, not the global's RBS type. Only a value written as a literal is judged — a number, a string, a symbol, an array, a hash or a regexp literal (interpolated or not), or `nil`, `true` or `false` — so a variable, a method call or a constant never reports, whatever its type. A literal stays silent when your program defines the method the setter asks for (`write`, `to_str`, `to_int`) or a `method_missing` / `respond_to_missing?` / `respond_to?` anywhere, in any spelling and on any class — Rigor cannot always tell which objects such a definition reaches, so it does not try — or when a top-level `include` / `extend` or an ancestor's `include` names a module Rigor has no RBS for. For `$stdout`, `$>` and `$stderr` it also stays silent where a `using` is in effect for a refinement that may add `write`. `$/`, `$-0`, `$,`, `$\` and `$~` accept only their classes, so none of that silences them. A write to `$stdin` is never checked, and a special any file aliases (`alias $stdout $out`, including a `pre_eval:` file) is exempt. `warning` under `lenient`. | high |
|
|
76
76
|
| <a id="rule-global-readonly-write"></a>`global.readonly-write` | A read-only special global is written — `$!`, `$$`, `$?`, `$<`, `$FILENAME`, `$*`, `$:` / `$LOAD_PATH` / `$-I`, `$"` / `$LOADED_FEATURES`, `$-W`, `$-p`, `$-l` or `$-a` — which raises `NameError` whatever the value. Covers `$g = value`, `$g += value` and a multiple-assignment target. `$LOAD_PATH ||= []` never writes and `$LOAD_PATH << dir` is a method call, so neither fires. A special any file aliases (`alias $! $err`) is exempt. An error in every profile. | high |
|
|
77
77
|
| <a id="rule-static-value-use-void"></a>`static.value-use.void` | A value recovered from an author-declared `-> void` return is used in value context (an assignment right-hand side, a call receiver, or a call argument). Off by default; reaches a run only through the `use-of-void-value` bleeding-edge feature (ADR-100). A bare-statement `void` call and a legitimate `top` value both stay silent. | high |
|
|
78
|
+
| <a id="rule-plugin-api-surface"></a>`plugin.api-surface` | Plugin code (inside a class that inherits `Rigor::Plugin::Base`) calls a member that a `Rigor::Plugin` interface (`_Scope`, `_Environment`, `_Services`, `_FactStore`, `_Configuration`) does not declare, on a value that interface types: a hook's `scope`, `scope.environment`, and so on. The engine object answers the call today, but it is outside the plugin API Rigor promises to keep, so call a member the interface declares (`sig/rigor/plugin/interfaces.rbs`) or a public namespace instead. Off by default; reaches a run through the `plugin-api-surface` bleeding-edge feature (ADR-128), or a `severity_overrides:` key naming `plugin.api-surface` itself (a bare `plugin` key does not reach it). Calls of members every object has (`nil?`, `respond_to?`) and values that lost the interface type (a helper parameter with no signature, a join with an untyped value) stay silent; a call a `respond_to?` guard protects still reports. | high |
|
|
78
79
|
| <a id="rule-effect-envelope-exceeded"></a>`effect.envelope-exceeded` | A method performs an effect its declared envelope does not admit — its proven effect labels (its own body plus everything it calls) are not covered by the `%a{pure}` or `%a{rigor:v1:effect …}` bound written on it or on its class. Opt-in twice over: it needs an `effects:` block in `.rigor.yml` and an envelope you wrote. Positioned at the Ruby `def`. Unproven ("and possibly more") effects never fire, and `mutate.local` is tolerated by every envelope. | high |
|
|
79
80
|
| <a id="rule-effect-liskov-widened"></a>`effect.liskov-widened` | An override escapes the envelope written on the method it overrides. A `PgRepo` is usable wherever a `Repo` is, so a `%a{rigor:v1:effect io.db}` on `Repo#find` binds `PgRepo#find` too: an implementation may be purer than the bound it inherits, never less pure. Either what the override *does* exceeds the inherited bound, or the envelope the override *declares for itself* is wider than it. Both sides must be authored — nothing fires unless someone wrote an envelope on the ancestor — and only subclassing counts, not `include`. Positioned at the override's `def`. Needs an `effects:` block. | high |
|
|
80
|
-
| <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or `.rigor.yml` for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
|
|
81
|
+
| <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or the loaded configuration file (`.rigor.yml` when none was loaded) for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
|
|
81
82
|
| <a id="rule-effect-annotations-unchecked"></a>`effect.annotations-unchecked` | Your signatures carry `%a{pure}` / `%a{rigor:v1:effect …}` but `.rigor.yml` has no `effects:` block, so nothing checks them. One `:info` per run, positioned at the first annotation. An annotation never turns effect collection on by itself — that would make one line in one file more expensive for every run — so this is how it tells you instead. Add `effects: {}` to opt in, or `disable:` it to keep the annotations documentary. Reads both annotation lanes — `sig/*.rbs` and rbs-inline comments — on every run, a warm `--incremental` with nothing changed included. | — |
|
|
82
83
|
| <a id="rule-rbs-contradicting-signature"></a>`rbs.contradicting-signature` | A method is declared both in your `sig/` and by an inline `# @rbs` / `#:` annotation, and the two provably contradict — no value or call satisfies both: in the return or a parameter every call must pass, the two types share no value (`::String` against `::Integer`). Only absolutely written Ruby core or stdlib classes count: a module such as `Comparable`, a class your project declares, a gem's class, or a relative name like plain `String` never does, or two keyword-free declarations accept positional counts that cannot meet, or one requires a keyword the other cannot take in any form. Overloads are paired by correspondence, not order. Also fires when a member-level `%a{rigor:v1:return: …}` / `%a{rigor:v1:param: …}` refinement shares no value with the type its own member declares. Positioned at the `.rbs` member; Rigor reads that declaration. A stale generated signature is the usual cause: regenerate it, or fix the annotation. Two declarations where one refines the other merge to the more precise one without a word, and a pair Rigor cannot rank drops the inline side with a `source-rbs-annotation-not-honoured` `:info` instead ([Precedence](plugins/rigor-rbs-inline.md#precedence)). An error in every profile. | high |
|
|
83
84
|
| <a id="rule-suppression-unknown-rule"></a>`suppression.unknown-rule` | A `# rigor:disable[-file]` comment names a rule that does not exist (typically a typo), so the suppression silently does nothing. `plugin.`-prefixed tokens are never flagged. | high |
|
|
@@ -130,7 +131,7 @@ re-stamps it for the run. Three profiles, set with the
|
|
|
130
131
|
| --- | --- |
|
|
131
132
|
| `lenient` | Only proven diagnostics are errors; uncertain ones drop to `warning` / `info`. For incremental adoption on legacy code. |
|
|
132
133
|
| `balanced` *(default)* | Most rules `error`; `dump.type` `info`; uncertain rules `warning`. |
|
|
133
|
-
| `strict` | Nearly every rule is an `error`. The exceptions: `call.self-undefined-method
|
|
134
|
+
| `strict` | Nearly every rule is an `error`. The exceptions: `call.self-undefined-method`, `static.value-use.void` and `plugin.api-surface` stay `off` (all three opt-in only), `flow.unreachable-clause` stays `warning` pending its false-positive gate, and the three `suppression.*` rules stay `warning` — a stale suppression comment is worth telling you about, but it is not a reason to fail a build. CI-friendly. |
|
|
134
135
|
|
|
135
136
|
Under `balanced`, the rules that do **not** emit as `error`
|
|
136
137
|
are:
|
|
@@ -139,7 +140,7 @@ are:
|
|
|
139
140
|
| --- | --- |
|
|
140
141
|
| `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker`, `effect.envelope-exceeded`, `effect.liskov-widened` |
|
|
141
142
|
| `info` | `flow.unreachable-clause`, `dump.type`, `effect.unknown-label`, `effect.annotations-unchecked` |
|
|
142
|
-
| `off` | `call.self-undefined-method`, `static.value-use.void` |
|
|
143
|
+
| `off` | `call.self-undefined-method`, `static.value-use.void`, `plugin.api-surface` |
|
|
143
144
|
|
|
144
145
|
Everything else emits as `error`. For one rule under all three
|
|
145
146
|
profiles, `rigor explain <rule>` prints `Authored severity:` and
|
data/docs/manual/08-skills.md
CHANGED
|
@@ -80,6 +80,17 @@ is not repeated here.)
|
|
|
80
80
|
minimal hand-RBS residual, under a double gate (the site becomes
|
|
81
81
|
protected *and* `rigor check` gains no new diagnostic). See
|
|
82
82
|
[Type-protection coverage](15-type-protection-coverage.md).
|
|
83
|
+
- **`rigor-rbs-contract`** — audits, repairs, and extends RBS
|
|
84
|
+
declarations as *contracts*: a gem's own `sig/`, an application's
|
|
85
|
+
`sig/`, or an entry contributed to `ruby/gem_rbs_collection`. It finds
|
|
86
|
+
where a declaration and the code disagree (`rigor check`'s
|
|
87
|
+
`def.return-type-mismatch`, `rigor sig-gen --diff`, use-site checks,
|
|
88
|
+
the library's documentation), changes a declaration only on cited
|
|
89
|
+
evidence, keeps a declared `void` unless the library documents a
|
|
90
|
+
value, and proves each change with a use-site test. On your own code
|
|
91
|
+
it asks you the contract questions the evidence leaves open; on a gem
|
|
92
|
+
you do not own, the gem's documentation decides. See
|
|
93
|
+
[ADR-131](../adr/131-declarations-are-contracts.md).
|
|
83
94
|
- **`rigor-baseline-reduce`** — works an existing `.rigor-baseline.yml`
|
|
84
95
|
down rule by rule. It prioritises with `rigor triage`, classifies each
|
|
85
96
|
site as a real bug / safe stylistic finding / false positive, and
|
|
@@ -35,6 +35,14 @@ install channels in [Installing Rigor](01-installation.md) provide it;
|
|
|
35
35
|
`mise` is the recommended one because its shims make `rigor` available
|
|
36
36
|
to GUI-launched editors that do not inherit your shell environment.
|
|
37
37
|
|
|
38
|
+
The [VS Code extension](../../editors/vscode/README.md) also finds a
|
|
39
|
+
mise or asdf shim when `rigor` is not on `PATH`. When it finds no
|
|
40
|
+
`rigor` at all, it offers to install a **managed Rigor** into its own
|
|
41
|
+
storage with a Ruby 4.0 already on the machine, and uses it as its
|
|
42
|
+
last resort; a project that pins `rigortype` in `Gemfile.lock`,
|
|
43
|
+
`mise.toml` or `.tool-versions` is never served by it. See the
|
|
44
|
+
extension README's *Server discovery*.
|
|
45
|
+
|
|
38
46
|
Do **not** add `rigortype` to your project's `Gemfile`. Rigor is a
|
|
39
47
|
tool, not a library — installing it standalone keeps its Ruby version
|
|
40
48
|
and its dependencies out of your application's resolution. See
|
|
@@ -61,7 +61,7 @@ mise use gem:rigortype
|
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
`mise use` records the chosen versions in a `mise.toml` in the
|
|
64
|
-
current directory and installs them in one step. Verify:
|
|
64
|
+
current directory and installs them in one step. Rails projects with Steep or `rbs_rails` list `rbs` in the bundle, and the `rbs` that `gem:rigortype` puts on `PATH` can shadow it; see the [install guide's note on `rbs` shadowing](../install.md#case-a--mise-is-available-recommended) before running `bundle exec rbs`. Verify:
|
|
65
65
|
|
|
66
66
|
```sh
|
|
67
67
|
rigor --version
|
|
@@ -108,10 +108,9 @@ annotations and no block gets told so, once per run:
|
|
|
108
108
|
sig/slug.rbs:2:1: info: Effect annotations (`%a{pure}` / `%a{rigor:v1:effect …}`) are present in your project's signatures, but `.rigor.yml` carries no `effects:` block, so effect collection never runs and nothing checks them — they are documentation, not a contract. Add `effects: {}` to have Rigor prove what your methods do and check these bounds against it (ADR-103); an annotation alone never turns collection on, because that would make one line in one signature file more expensive for every run of the project.
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
From **v0.
|
|
112
|
-
all behaves as `effects: {}`, and `effects: false` is how you opt out.
|
|
113
|
-
|
|
114
|
-
`bleeding_edge: [effects-on-by-default]`.
|
|
111
|
+
From **v0.5.0** collection is the default: a config with no `effects:` key at
|
|
112
|
+
all behaves as `effects: {}`, and `effects: false` is how you opt out. Until
|
|
113
|
+
then you can preview that with `bleeding_edge: [effects-on-by-default]`.
|
|
115
114
|
|
|
116
115
|
One exception to all of this: `rigor effects` — the bare report — runs under an
|
|
117
116
|
implicit empty block whether or not you configured one, so you can look before
|
|
@@ -399,12 +399,21 @@ diff — not a policy scoped to `app/views/**/*`. See
|
|
|
399
399
|
those producers widens what this plugin can check.
|
|
400
400
|
- **`params[:key]` is a gradual carrier.** Inside a controller,
|
|
401
401
|
`params` types as `ActionController::Parameters`, and so does the
|
|
402
|
-
result of every builder method that always returns one — `
|
|
403
|
-
`permit`, `
|
|
404
|
-
`
|
|
405
|
-
`
|
|
406
|
-
`
|
|
407
|
-
|
|
402
|
+
result of every builder method that always returns one — `permit`,
|
|
403
|
+
`permit!`, `slice`, `slice!`, `except`, `without`, `extract!`,
|
|
404
|
+
`merge`, `merge!`, `reverse_merge`, `reverse_merge!`,
|
|
405
|
+
`with_defaults`, `with_defaults!`, `compact`, `compact_blank`,
|
|
406
|
+
`deep_dup` — when the receiver is `params` itself. A carrier
|
|
407
|
+
(`params[:tags]`) may hold an Array or a String at runtime, so a
|
|
408
|
+
builder call on one answers the carrier too, except `permit` and
|
|
409
|
+
`permit!`, which no core class defines and which answer `Parameters`.
|
|
410
|
+
`require`
|
|
411
|
+
and `expect` are different: Rails returns the value itself, which
|
|
412
|
+
can be a scalar (`params.require(:id).to_sym`), so they answer the
|
|
413
|
+
same `Dynamic[ActionController::Parameters]` carrier (#2947). A
|
|
414
|
+
`permit` or `permit!` after one (`params.require(:user).permit(:name)`)
|
|
415
|
+
still types as `Parameters`; other chain methods off a carrier stay
|
|
416
|
+
the carrier. A subscript read answers
|
|
408
417
|
`Dynamic[ActionController::Parameters | nil]` (#2430): the static
|
|
409
418
|
facet is the nilable `Parameters`, so `params[:user][:name]` and
|
|
410
419
|
`params[:user].permit(:name)` keep dispatching on that facet, and
|
|
@@ -53,6 +53,9 @@ Roughly the top ~40 selectors plus their close neighbours, across:
|
|
|
53
53
|
`TrueClass` / `FalseClass`).
|
|
54
54
|
- **Integer / Float** — Duration multipliers (`#days`, `#hours`,
|
|
55
55
|
`#minutes`, …) and Bytes multipliers (`#megabytes`, `#gigabytes`, …).
|
|
56
|
+
- **Integer / Float / BigDecimal** — `#to_fs` / `#to_formatted_s`
|
|
57
|
+
(`NumericWithFormat`: a Symbol format, an Integer radix or a String
|
|
58
|
+
format, and the no-argument `to_s` form).
|
|
56
59
|
- **String** — inflections (`#underscore`, `#camelize`, `#classify`,
|
|
57
60
|
`#constantize`, `#pluralize`, …), filters (`#squish`, `#truncate`),
|
|
58
61
|
`#html_safe`, `#starts_with?` / `#ends_with?` (also on **Symbol**),
|
|
@@ -61,9 +61,23 @@ The plugin also types the annotation DSL's own expressions
|
|
|
61
61
|
`T::Struct` subclass still needs the subclass's own `extend T::Sig`,
|
|
62
62
|
matching the runtime) through a bundled
|
|
63
63
|
`sorbet-runtime` RBS surface — so the sig DSL no longer reads as
|
|
64
|
-
`Dynamic[top]` on a Sorbet codebase.
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
`Dynamic[top]` on a Sorbet codebase.
|
|
65
|
+
|
|
66
|
+
On a `T::Struct`, `T::InexactStruct` or `T::ImmutableStruct`
|
|
67
|
+
subclass, `prop :name, Type` and `const :name, Type` at the top
|
|
68
|
+
level of the class body give `obj.name` the translated `Type`
|
|
69
|
+
(`T.nilable(String)` reads `String?`; a form outside the table
|
|
70
|
+
reads `Dynamic[top]`), and `prop` also answers `obj.name = v`.
|
|
71
|
+
A relative constant in `Type` resolves from the class body's
|
|
72
|
+
namespaces outward, and one that names no known class reads
|
|
73
|
+
`Dynamic[top]`.
|
|
74
|
+
A `const` has no writer, so `obj.name = v` still reports.
|
|
75
|
+
Subclasses inherit the accessors, a `def` of the same name in
|
|
76
|
+
the class body wins over the macro, and a `prop` nested under an
|
|
77
|
+
`if` or a block reads `Dynamic[top]`. A `prop` call in a class
|
|
78
|
+
outside the struct family defines nothing.
|
|
79
|
+
|
|
80
|
+
What it does not model: `include T::Props` does
|
|
67
81
|
not confer the `ClassMethods` surface (only the `< T::Struct`
|
|
68
82
|
subclass path does), and the deeper type-member machinery
|
|
69
83
|
(`T.attached_class` variance, `type_member` bounds) is not
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../reflection"
|
|
6
|
+
require_relative "../../source/node_children"
|
|
7
|
+
require_relative "../../type"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Analysis
|
|
11
|
+
module CheckRules
|
|
12
|
+
# Issue #3036 — answers, for a call node `flow.always-raises` proved raises, whether an enclosing
|
|
13
|
+
# `begin` (or `def … rescue … end`, which Prism wraps in the same `BeginNode`) catches the raise
|
|
14
|
+
# before it escapes. When a rescue clause may catch the raised class the exception flows to the
|
|
15
|
+
# handler, so the rule stays silent.
|
|
16
|
+
#
|
|
17
|
+
# The check is deliberately conservative in BOTH directions:
|
|
18
|
+
#
|
|
19
|
+
# - it suppresses only when a clause NAMES a class the raised class orders at-or-below
|
|
20
|
+
# (`rescue ZeroDivisionError`, `rescue StandardError`, `rescue Exception`, or a bare `rescue`,
|
|
21
|
+
# which is `StandardError`), resolved through the typer so lexical constant lookup applies. An
|
|
22
|
+
# exception reference that is not a constant (`rescue *ERRORS`, `rescue klass`) contributes no
|
|
23
|
+
# coverage, and an unresolvable / `:unknown` ordering keeps the diagnostic — a wrong silence is
|
|
24
|
+
# worse than a kept finding.
|
|
25
|
+
# - it never crosses a body boundary ({BODY_BOUNDARIES}): a `def`, `->`, or block body runs when
|
|
26
|
+
# called, not while the `begin` body evaluates, and `BEGIN` / `END` blocks run on their own
|
|
27
|
+
# schedule, so an outer rescue cannot be proven to see their raise. Class / module bodies are
|
|
28
|
+
# NOT boundaries — they evaluate inline, so their raises DO reach the enclosing rescue.
|
|
29
|
+
#
|
|
30
|
+
# Prism nodes carry no parent pointers, so ancestry is answered from a `node => parent` table
|
|
31
|
+
# built lazily on the first query: a file with no zero-division candidate never pays for it.
|
|
32
|
+
class AlwaysRaisesShield
|
|
33
|
+
# The exception class the rule proves today — Integer `/# % div modulo divmod` by a literal
|
|
34
|
+
# zero divisor. A rescue catches it when the clause names the class itself or an ancestor.
|
|
35
|
+
RAISED_CLASS = "ZeroDivisionError"
|
|
36
|
+
|
|
37
|
+
# The implicit class of a bare `rescue` / `rescue => e`.
|
|
38
|
+
BARE_RESCUE_CLASS = "StandardError"
|
|
39
|
+
|
|
40
|
+
# Node classes whose bodies do not execute inside the enclosing `begin` — see the class
|
|
41
|
+
# comment. Mirrors the deferred-body set `Inference::ScopeIndexer` already tracks.
|
|
42
|
+
BODY_BOUNDARIES = [
|
|
43
|
+
Prism::DefNode, Prism::LambdaNode, Prism::BlockNode,
|
|
44
|
+
Prism::PreExecutionNode, Prism::PostExecutionNode
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
def initialize(root)
|
|
48
|
+
@root = root
|
|
49
|
+
@parent_map = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# True when `node` sits in the protected body of a `begin` whose rescue chain may catch
|
|
53
|
+
# {RAISED_CLASS}. `scope` is the scope recorded for `node`; it resolves the clause's exception
|
|
54
|
+
# constants lexically. A nil scope or a node outside `root`'s subtree answers false.
|
|
55
|
+
def rescued?(node, scope)
|
|
56
|
+
return false if scope.nil?
|
|
57
|
+
|
|
58
|
+
parents = parent_map
|
|
59
|
+
child = node
|
|
60
|
+
current = parents[node]
|
|
61
|
+
while current
|
|
62
|
+
return true if protected_body?(current, child, scope)
|
|
63
|
+
return false if BODY_BOUNDARIES.any? { |klass| current.is_a?(klass) }
|
|
64
|
+
|
|
65
|
+
child = current
|
|
66
|
+
current = parents[current]
|
|
67
|
+
end
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# True when `node` is a `begin` whose protected body (`statements`) contains `child` — the
|
|
74
|
+
# ascent arrived through the body rather than through a rescue / else / ensure clause, whose
|
|
75
|
+
# raises this `begin` does not catch — and whose rescue chain may catch {RAISED_CLASS}. When
|
|
76
|
+
# the chain does NOT catch, the ascent continues: an outer `begin` may still.
|
|
77
|
+
def protected_body?(node, child, scope)
|
|
78
|
+
return false unless node.is_a?(Prism::BeginNode)
|
|
79
|
+
return false unless child.equal?(node.statements)
|
|
80
|
+
return false if node.rescue_clause.nil?
|
|
81
|
+
|
|
82
|
+
rescue_chain_catches?(node.rescue_clause, scope)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Any clause of the `rescue … rescue …` chain may catch — clauses are tried in order and the
|
|
86
|
+
# first match wins, so one catching clause is enough.
|
|
87
|
+
def rescue_chain_catches?(clause, scope)
|
|
88
|
+
while clause
|
|
89
|
+
return true if rescue_clause_catches?(clause, scope)
|
|
90
|
+
|
|
91
|
+
clause = clause.subsequent
|
|
92
|
+
end
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# One `rescue` clause catches {RAISED_CLASS} when any class it names orders at-or-above the
|
|
97
|
+
# raised class. A bare `rescue` names {BARE_RESCUE_CLASS}.
|
|
98
|
+
def rescue_clause_catches?(clause, scope)
|
|
99
|
+
exceptions = clause.exceptions
|
|
100
|
+
return class_catches?(BARE_RESCUE_CLASS, scope) if exceptions.empty?
|
|
101
|
+
|
|
102
|
+
exceptions.any? do |exception|
|
|
103
|
+
class_name = exception_class_name(exception, scope)
|
|
104
|
+
class_name && class_catches?(class_name, scope)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The class a rescue exception reference names, or nil when it is not a constant path (a splat,
|
|
109
|
+
# a dynamic expression) or does not resolve to a class object. Only `Type::Singleton` counts:
|
|
110
|
+
# the rescue operand is the class itself, and an instance-typed constant cannot be proven to
|
|
111
|
+
# match.
|
|
112
|
+
def exception_class_name(node, scope)
|
|
113
|
+
return nil unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
114
|
+
|
|
115
|
+
type = scope.type_of(node)
|
|
116
|
+
type.class_name if type.is_a?(Type::Singleton)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `class_ordering(raised, rescued)`: `:equal` / `:subclass` mean the rescued class IS the raised
|
|
120
|
+
# class or an ancestor of it — the clause catches. `:superclass`, `:disjoint`, and `:unknown`
|
|
121
|
+
# all keep the diagnostic.
|
|
122
|
+
def class_catches?(rescued_class, scope)
|
|
123
|
+
%i[equal subclass].include?(
|
|
124
|
+
Rigor::Reflection.class_ordering(RAISED_CLASS, rescued_class, scope: scope)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# `node => parent` for every descendant of `root`, built on first use. Iterative so a deeply
|
|
129
|
+
# nested file cannot overflow the stack the way a recursive walk could.
|
|
130
|
+
def parent_map
|
|
131
|
+
@parent_map ||= begin
|
|
132
|
+
parents = {}
|
|
133
|
+
stack = [@root]
|
|
134
|
+
until stack.empty?
|
|
135
|
+
node = stack.pop
|
|
136
|
+
node.rigor_each_child do |child|
|
|
137
|
+
parents[child] = node
|
|
138
|
+
stack << child
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
parents
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
module CheckRules
|
|
6
|
+
# Issue #2786 — what the per-call-node rules ({CheckRules.call_node_diagnostics}) each derived on their own:
|
|
7
|
+
# the call node's index scope, the type of its receiver under that scope, and the RBS signature the call
|
|
8
|
+
# resolves to on that receiver. One object per call node is built before the rules run and handed to every
|
|
9
|
+
# rule that reads them.
|
|
10
|
+
#
|
|
11
|
+
# The receiver type is computed lazily, on the first rule that asks, from the same `(scope, node)` pair each
|
|
12
|
+
# rule typed before: `scope_index[call_node]` and `call_node.receiver`. Every later rule reads that value
|
|
13
|
+
# instead of re-typing the receiver chain. This is not a memo over `type_of`: nothing is served across call
|
|
14
|
+
# nodes or across scopes, and no fact is computed anywhere but at the first point a rule computed it before.
|
|
15
|
+
# It rests on the `Scope#type_of` contract that one `(scope, node)` pair types structurally equal within a run
|
|
16
|
+
# *under the same ambient inputs* (`docs/internal-spec/inference-engine.md`), which holds here because of
|
|
17
|
+
# an invariant of the rule list: between the first rule that types the receiver and the last that reads it, no
|
|
18
|
+
# frame is installed and nothing types a node inside the receiver's subtree under another state or adds an
|
|
19
|
+
# `optimistic_origins` mark there. The other typings in that window are the call's arguments (outside the
|
|
20
|
+
# subtree), `receiver.receiver` under the same scope (which rewrites what the receiver's typing already
|
|
21
|
+
# wrote), and the key-presence re-walk, which runs on `with_isolated_side_tables`. A rule inserted between
|
|
22
|
+
# two readers that breaks this must type the receiver itself.
|
|
23
|
+
#
|
|
24
|
+
# Of the typing, only the receiver is shared. A rule that types a sub-expression (`receiver.receiver`), an
|
|
25
|
+
# argument, or any node under a scope other than the call's keeps its own `type_of` call: arguments are typed
|
|
26
|
+
# from their own index entry by `call.argument-type-mismatch` and from the call's by `flow.always-raises`, and
|
|
27
|
+
# the two meet only on a divisor.
|
|
28
|
+
class CallFacts
|
|
29
|
+
attr_reader :call_node, :scope_index, :scope
|
|
30
|
+
|
|
31
|
+
def initialize(call_node, scope_index)
|
|
32
|
+
@call_node = call_node
|
|
33
|
+
@scope_index = scope_index
|
|
34
|
+
@scope = scope_index[call_node]
|
|
35
|
+
@receiver_type = nil
|
|
36
|
+
@receiver_signature = nil
|
|
37
|
+
@receiver_signature_resolved = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# `scope.type_of(call_node.receiver)`, computed once. Callers check that the call has a receiver and that
|
|
41
|
+
# {#scope} is not nil first, as they did before sharing it.
|
|
42
|
+
def receiver_type
|
|
43
|
+
@receiver_type ||= @scope.type_of(@call_node.receiver)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The signature `CheckRules.receiver_signature` resolves, its block run on the first ask only. A nil answer
|
|
47
|
+
# is remembered too. A block that raises leaves it unresolved, so the next ask raises as it did before.
|
|
48
|
+
def receiver_signature
|
|
49
|
+
return @receiver_signature if @receiver_signature_resolved
|
|
50
|
+
|
|
51
|
+
signature = yield
|
|
52
|
+
@receiver_signature_resolved = true
|
|
53
|
+
@receiver_signature = signature
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -59,6 +59,14 @@ module Rigor
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Issue #2656 — true when `node` reads a local a class guard narrowed from a `Dynamic` value
|
|
63
|
+
# ({Scope#with_gradual_narrowing_mark}). `call.argument-type-mismatch` judges such an argument by its class
|
|
64
|
+
# rather than against the parameter's literals (`CheckRules#gradual_narrowing_accepted?`); the other rules
|
|
65
|
+
# read the narrowed `Nominal` as before.
|
|
66
|
+
def gradual_narrowed?(node, scope)
|
|
67
|
+
node.is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(node.name)
|
|
68
|
+
end
|
|
69
|
+
|
|
62
70
|
# The globals whose declared seeds `node` still reads, empty for none: a read of a marked global, or of a
|
|
63
71
|
# local copied from one or more (`sep = $/`, `Scope#declaration_sourced_global_copies`), bare or in
|
|
64
72
|
# parentheses (`($stdout)`).
|
|
@@ -6,6 +6,7 @@ require_relative "rule_walk"
|
|
|
6
6
|
require_relative "../../scope/resolution_chain"
|
|
7
7
|
require_relative "../../source/constant_path"
|
|
8
8
|
require_relative "../../source/node_children"
|
|
9
|
+
require_relative "../../inference/census_foreign_block"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Analysis
|
|
@@ -28,6 +29,9 @@ module Rigor
|
|
|
28
29
|
# doesn't yet model.
|
|
29
30
|
# - Nested classes / modules / defs inside a method body are barriers, mirroring the indexer's
|
|
30
31
|
# `IVAR_BARRIER_NODES` policy.
|
|
32
|
+
# - Issue #2943: a write directly in a block or `class << obj` body whose `self` is provably not an instance
|
|
33
|
+
# of the class (`Other.new.instance_eval { @r = 1 }`), by the class-ivar census's rule
|
|
34
|
+
# ({Inference::CensusForeignBlock}).
|
|
31
35
|
class IvarWriteCollector
|
|
32
36
|
BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
33
37
|
private_constant :BARRIER_NODES
|
|
@@ -156,7 +160,8 @@ module Rigor
|
|
|
156
160
|
|
|
157
161
|
def collect_def_writes(def_node, qualified_prefix)
|
|
158
162
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
159
|
-
|
|
163
|
+
# A def with a receiver writes the ivars of another object than an instance (#3155).
|
|
164
|
+
return if def_node.receiver
|
|
160
165
|
|
|
161
166
|
class_name = qualified_prefix.join("::")
|
|
162
167
|
gather_writes(def_node.body, class_name, def_node.name)
|
|
@@ -168,7 +173,13 @@ module Rigor
|
|
|
168
173
|
record_write(node, class_name, method_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
169
174
|
return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
170
175
|
|
|
171
|
-
|
|
176
|
+
# #2943 — a block or `class <<` body whose `self` is provably another object writes that object's ivars, as
|
|
177
|
+
# the class-ivar census reads it ({Inference::CensusForeignBlock}); a `define_method` block nested in it that
|
|
178
|
+
# runs on an instance again still counts. A `def` it reenters stays a barrier here.
|
|
179
|
+
children = Inference::CensusForeignBlock.census_children(node, class_name)
|
|
180
|
+
return node.rigor_each_child { |child| gather_writes(child, class_name, method_name) } unless children
|
|
181
|
+
|
|
182
|
+
children.each { |child| gather_writes(child, class_name, method_name) }
|
|
172
183
|
end
|
|
173
184
|
|
|
174
185
|
def record_write(node, class_name, method_name)
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../../inference/scope_indexer"
|
|
6
6
|
require_relative "../../source/node_children"
|
|
7
7
|
require_relative "rebinding_block_callees"
|
|
8
|
+
require_relative "replaced_singleton_defs"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Analysis
|
|
@@ -42,6 +43,11 @@ module Rigor
|
|
|
42
43
|
|
|
43
44
|
def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
|
|
44
45
|
|
|
46
|
+
# Issue #3155 — the file's singleton defs a later certain def replaces, for `def.return-type-mismatch`.
|
|
47
|
+
def replaced_singleton_defs(scope_index)
|
|
48
|
+
@replaced_singleton_defs ||= ReplacedSingletonDefs.new(@root, scope_index)
|
|
49
|
+
end
|
|
50
|
+
|
|
45
51
|
# Issue #1817 — the file's block-passing calls, for `call.unresolved-toplevel`'s rebinding-callee decline.
|
|
46
52
|
def block_calls = (@block_calls ||= RebindingBlockCallees::Sites.new(@root))
|
|
47
53
|
|