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
|
@@ -65,6 +65,14 @@ module Rigor
|
|
|
65
65
|
manifest(
|
|
66
66
|
id: "activerecord",
|
|
67
67
|
target_gems: ["activerecord"],
|
|
68
|
+
# 0.16.0, 2026-11-13 (#2956) — the `id` column takes the primary key's type (`id: :uuid` reads `String`)
|
|
69
|
+
# instead of always `Integer`, and `serial` / `bigserial` columns read `Integer`. The schema table
|
|
70
|
+
# payload changes, so the bump follows the 0.15.0 entry.
|
|
71
|
+
#
|
|
72
|
+
# 0.15.0, 2026-11-07 (#2945) — `create_table`'s `primary_key: "name"` registers that column, so a query
|
|
73
|
+
# on it, or on an `alias_attribute` targeting it, no longer reports `unknown-column`. The schema table
|
|
74
|
+
# payload changes, and the bump (part of the producer cache KEY) stops a warm run serving the old one.
|
|
75
|
+
#
|
|
68
76
|
# 0.14.0, 2026-10-10 (#1721) — the plugin publishes `:reachability_roots`: the models ActiveRecord loads
|
|
69
77
|
# by name from an association. The association rows gain `macro`, `class_name_option`, `through` and
|
|
70
78
|
# `source_type_option`, so a cached 0.13.0 payload lacks the fields the roots are derived from and
|
|
@@ -124,7 +132,7 @@ module Rigor
|
|
|
124
132
|
# a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
|
|
125
133
|
# instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
|
|
126
134
|
# `:select` added to the relation-entry-point list.
|
|
127
|
-
version: "0.
|
|
135
|
+
version: "0.16.0",
|
|
128
136
|
description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
|
|
129
137
|
config_schema: {
|
|
130
138
|
"schema_file" => { kind: :string, default: "db/schema.rb" },
|
|
@@ -491,8 +499,8 @@ module Rigor
|
|
|
491
499
|
# The project's OWN definition wins, and is not merely a precision preference: `def name` on the
|
|
492
500
|
# model — or on an ancestor it declares — is an override Ruby dispatches to, and the plugin tier sits
|
|
493
501
|
# ABOVE the engine's body-inference tiers, so answering here would displace the override's real return
|
|
494
|
-
# type with `Dynamic`. `
|
|
495
|
-
# discovery arm reads, and it fails toward declining (a budget-exhausted walk answers
|
|
502
|
+
# type with `Dynamic`. `Reflection.project_method_defined(source: :discovered)` is the same table the veto's own
|
|
503
|
+
# discovery arm reads, and it fails toward declining (a budget-exhausted walk answers `maybe`).
|
|
496
504
|
def implicit_self_instance_member_type(call_node, scope, index)
|
|
497
505
|
return nil if scope.nil?
|
|
498
506
|
return nil unless call_node.arguments.nil?
|
|
@@ -504,7 +512,9 @@ module Rigor
|
|
|
504
512
|
entry = index.find(self_type.class_name)
|
|
505
513
|
return nil if entry.nil?
|
|
506
514
|
return nil unless instance_member_name?(entry, call_node.name)
|
|
507
|
-
return nil
|
|
515
|
+
return nil unless Rigor::Reflection.project_method_defined(
|
|
516
|
+
self_type.class_name, call_node.name, kind: :instance, source: :discovered, scope: scope
|
|
517
|
+
).no?
|
|
508
518
|
return nil if rbs_answers_before_object?(self_type.class_name, call_node.name, scope)
|
|
509
519
|
|
|
510
520
|
Rigor::Type::Combinator.untyped
|
|
@@ -552,6 +562,18 @@ module Rigor
|
|
|
552
562
|
!entry.column(column_name).nil?
|
|
553
563
|
end
|
|
554
564
|
|
|
565
|
+
# A project `self.find` on the model or an ancestor, or a walk that ran out of budget before ruling one out
|
|
566
|
+
# (`maybe`): the veto fails toward declining, as the discovery walk always has.
|
|
567
|
+
def project_find_defined?(entry, scope)
|
|
568
|
+
project_singleton_defined?(entry, :find, scope)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# The same veto for any class-side selector the plugin answers in Rails' name (`find`, `select`).
|
|
572
|
+
def project_singleton_defined?(entry, name, scope)
|
|
573
|
+
!Rigor::Reflection.project_method_defined(entry.class_name, name, kind: :singleton, source: :discovered,
|
|
574
|
+
scope: scope).no?
|
|
575
|
+
end
|
|
576
|
+
|
|
555
577
|
# Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
|
|
556
578
|
# them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model;
|
|
557
579
|
# `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
|
|
@@ -572,7 +594,7 @@ module Rigor
|
|
|
572
594
|
|
|
573
595
|
# A model that defines its own `self.find` (`find(owner, id)`) takes two arguments for its own
|
|
574
596
|
# reasons, so the Array reading of Rails' `find` is withheld and the project's method answers.
|
|
575
|
-
return nil if
|
|
597
|
+
return nil if project_find_defined?(entry, scope)
|
|
576
598
|
|
|
577
599
|
Rigor::Type::Combinator.nominal_of("Array", type_args: [model])
|
|
578
600
|
when :find_by!
|
|
@@ -584,13 +606,10 @@ module Rigor
|
|
|
584
606
|
Rigor::Type::Combinator.nominal_of(entry.class_name),
|
|
585
607
|
Rigor::Type::Combinator.constant_of(nil)
|
|
586
608
|
)
|
|
587
|
-
when :
|
|
588
|
-
|
|
589
|
-
#
|
|
590
|
-
#
|
|
591
|
-
# multiplexer, return `Array[String]`), masking the AR class-side relation entry point. The rest
|
|
592
|
-
# of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a relation is
|
|
593
|
-
# open.
|
|
609
|
+
when :select then class_select_return_type(call_node, entry, scope)
|
|
610
|
+
when :where, :all, :order, :limit, :none
|
|
611
|
+
# The rest of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a
|
|
612
|
+
# relation is open.
|
|
594
613
|
relation_of(entry.class_name)
|
|
595
614
|
when :table_name
|
|
596
615
|
table_name_return_type(entry)
|
|
@@ -601,6 +620,23 @@ module Rigor
|
|
|
601
620
|
end
|
|
602
621
|
end
|
|
603
622
|
|
|
623
|
+
# `:select` was added to close Mastodon's `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }`
|
|
624
|
+
# shape: the implicit-self `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select`
|
|
625
|
+
# (IO multiplexer, return `Array[String]`), masking the AR class-side relation entry point.
|
|
626
|
+
#
|
|
627
|
+
# `Model.select` is `all.select`, so a block makes it `Enumerable#select` over the loaded records, as the
|
|
628
|
+
# bundled `Relation#select` overload says (#1408), and the answer is an Array of the model. Fields next to a
|
|
629
|
+
# block answer the Array too, for the reason given on that overload. A model that defines its own
|
|
630
|
+
# `self.select` keeps its own answer for the block forms, as it does for `find`'s block and multi-id
|
|
631
|
+
# forms (`project_find_defined?`); the builder form stays the relation, as `find`'s one-id form stays the
|
|
632
|
+
# model.
|
|
633
|
+
def class_select_return_type(call_node, entry, scope)
|
|
634
|
+
return relation_of(entry.class_name) if call_node.block.nil?
|
|
635
|
+
return nil if project_singleton_defined?(entry, :select, scope)
|
|
636
|
+
|
|
637
|
+
Rigor::Type::Combinator.nominal_of("Array", type_args: [Rigor::Type::Combinator.nominal_of(entry.class_name)])
|
|
638
|
+
end
|
|
639
|
+
|
|
604
640
|
# `Model.find { |m| … }` / `Model.find(&:pred?)` never reaches the id lookup: `Core.find` passes a
|
|
605
641
|
# block to `super` (`all.find`), whose `Relation#find` does `return super if block_given?`, landing on
|
|
606
642
|
# `Enumerable#find` over the loaded records. So the answer is the element or `nil`, exactly what the
|
|
@@ -615,7 +651,7 @@ module Rigor
|
|
|
615
651
|
# `Relation#find` overload does. A project `self.find` keeps its own answer, as it does for the
|
|
616
652
|
# multi-id form.
|
|
617
653
|
def block_find_return_type(call_node, entry, scope)
|
|
618
|
-
return nil if
|
|
654
|
+
return nil if project_find_defined?(entry, scope)
|
|
619
655
|
return Rigor::Type::Combinator.untyped unless call_argument_count(call_node).zero?
|
|
620
656
|
|
|
621
657
|
Rigor::Type::Combinator.union(
|
|
@@ -231,12 +231,26 @@ module ActiveRecord
|
|
|
231
231
|
def left_outer_joins: (*untyped) -> self
|
|
232
232
|
%a{pure}
|
|
233
233
|
def references: (*untyped) -> self
|
|
234
|
-
# With a block, `select` is Enumerable
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
234
|
+
# With a block, `select` is `Enumerable#select` over the loaded
|
|
235
|
+
# records and returns an Array (`return super()` when a block is
|
|
236
|
+
# given, `relation/query_methods.rb:413` in activerecord 8.1.3.1).
|
|
237
|
+
# Fields and a block together raise `ArgumentError` there; Rails 4
|
|
238
|
+
# ignored the fields and returned the Array, so that form answers
|
|
239
|
+
# the Array too, which holds on either version. Without a block it
|
|
240
|
+
# is the query builder.
|
|
241
|
+
#
|
|
242
|
+
# `Array[Elem]` is a closed receiver, unlike this one: an
|
|
243
|
+
# ActiveSupport Array method on the result (`pluck`, `second`,
|
|
244
|
+
# `to_sentence`, `blank?`) resolves only with the
|
|
245
|
+
# `rigor-activesupport-core-ext` plugin enabled, and reports
|
|
246
|
+
# `call.undefined-method` without it, as it does after `to_a`.
|
|
247
|
+
#
|
|
248
|
+
# The `%a{pure}` is the builder form's. The block form's
|
|
249
|
+
# `io.db.read` comes from the plugin's `RELATION_MATERIALIZERS` row,
|
|
250
|
+
# which labels both forms today (#1318).
|
|
238
251
|
%a{pure}
|
|
239
|
-
def select: (*untyped) ->
|
|
252
|
+
def select: (*untyped) { (Elem) -> boolish } -> Array[Elem]
|
|
253
|
+
| (*untyped) -> self
|
|
240
254
|
%a{pure}
|
|
241
255
|
def reselect: (*untyped) -> self
|
|
242
256
|
%a{pure}
|
|
@@ -89,13 +89,15 @@ module Rigor
|
|
|
89
89
|
# decline — they take side-effecting argument types that the RBS surface already covers.
|
|
90
90
|
dynamic_return receivers: -> { attachment_index&.class_names || [] } do |call_node, scope|
|
|
91
91
|
next nil unless call_node.is_a?(Prism::CallNode)
|
|
92
|
-
|
|
92
|
+
|
|
93
|
+
receiver = call_node.receiver
|
|
94
|
+
next nil if receiver.nil?
|
|
93
95
|
next nil unless call_node.arguments.nil?
|
|
94
96
|
|
|
95
97
|
index = attachment_index
|
|
96
98
|
next nil if index.nil? || index.empty?
|
|
97
99
|
|
|
98
|
-
receiver_type = scope.type_of(
|
|
100
|
+
receiver_type = scope.type_of(receiver)
|
|
99
101
|
next nil unless receiver_type.is_a?(Rigor::Type::Nominal)
|
|
100
102
|
|
|
101
103
|
# `AttachmentIndex#attachments_for` de-roots the query itself (#621) — no retry arm here.
|
|
@@ -132,10 +132,12 @@ module Rigor
|
|
|
132
132
|
|
|
133
133
|
dynamic_return methods: DURATION_MULTIPLIERS do |call_node, scope|
|
|
134
134
|
next nil unless call_node.is_a?(Prism::CallNode)
|
|
135
|
-
|
|
135
|
+
|
|
136
|
+
receiver = call_node.receiver
|
|
137
|
+
next nil unless receiver # `day` with no receiver is somebody's own method
|
|
136
138
|
next nil unless call_node.arguments.nil?
|
|
137
139
|
next nil unless call_node.block.nil?
|
|
138
|
-
next nil unless numeric_receiver?(scope&.type_of(
|
|
140
|
+
next nil unless numeric_receiver?(scope&.type_of(receiver))
|
|
139
141
|
|
|
140
142
|
Rigor::Type::Combinator.nominal_of(DURATION_NOMINAL)
|
|
141
143
|
end
|
|
@@ -200,7 +202,11 @@ module Rigor
|
|
|
200
202
|
argument_kind = duration_operand_kind(scope&.type_of(argument))
|
|
201
203
|
next nil if argument_kind.nil?
|
|
202
204
|
|
|
203
|
-
|
|
205
|
+
receiver = call_node.receiver
|
|
206
|
+
# Prism gives every parsed `+` / `-` / `*` call a receiver; the guard is for the RBS `Node?` type.
|
|
207
|
+
next nil if receiver.nil?
|
|
208
|
+
|
|
209
|
+
receiver_kind = duration_operand_kind(scope&.type_of(receiver))
|
|
204
210
|
next nil if receiver_kind.nil?
|
|
205
211
|
|
|
206
212
|
duration_arithmetic_result(call_node.name, receiver_kind, argument_kind)
|
|
@@ -210,11 +216,13 @@ module Rigor
|
|
|
210
216
|
# member; see {SymbolizeKeys}. The RBS rows answer `Hash[untyped, V]` wherever this declines.
|
|
211
217
|
dynamic_return methods: %i[symbolize_keys to_options] do |call_node, scope|
|
|
212
218
|
next nil unless call_node.is_a?(Prism::CallNode)
|
|
213
|
-
|
|
219
|
+
|
|
220
|
+
receiver = call_node.receiver
|
|
221
|
+
next nil unless receiver
|
|
214
222
|
next nil unless call_node.arguments.nil?
|
|
215
223
|
|
|
216
224
|
# A block is ignored by `symbolize_keys`, so it does not decline.
|
|
217
|
-
SymbolizeKeys.result(scope&.type_of(
|
|
225
|
+
SymbolizeKeys.result(scope&.type_of(receiver), scope)
|
|
218
226
|
end
|
|
219
227
|
|
|
220
228
|
# ADR-88 WD1 — both tables are static and the RBS bundle ships with the gem; nothing here reads a
|
|
@@ -604,6 +604,35 @@ class Float
|
|
|
604
604
|
def gigabytes: () -> Float
|
|
605
605
|
end
|
|
606
606
|
|
|
607
|
+
# ---------------------------------------------------------------
|
|
608
|
+
# NumericWithFormat — ActiveSupport's `core_ext/numeric/conversions`, included into Integer, Float and
|
|
609
|
+
# BigDecimal. Declared on the module and included, not as a `def` on each class, so a project `sig/` that
|
|
610
|
+
# declares `Integer#to_fs` itself overrides it instead of raising `DuplicatedMethodDefinitionError` (the
|
|
611
|
+
# `process.rbs` shape). Not pure: the Symbol formats read `I18n` for their locale. An Integer or String
|
|
612
|
+
# format is `to_s(format)`, and no format is `to_s`, so every form answers a String. `options` is any
|
|
613
|
+
# Hash-like value: ActiveSupport calls `symbolize_keys` on it, so String keys and a HashWithIndifferentAccess
|
|
614
|
+
# are both valid.
|
|
615
|
+
# ---------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
module ActiveSupport
|
|
618
|
+
module NumericWithFormat
|
|
619
|
+
def to_fs: (?(Symbol | Integer | String | nil) format, ?Hash[untyped, untyped]? options) -> String
|
|
620
|
+
def to_formatted_s: (?(Symbol | Integer | String | nil) format, ?Hash[untyped, untyped]? options) -> String
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
class Integer
|
|
625
|
+
include ActiveSupport::NumericWithFormat
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
class Float
|
|
629
|
+
include ActiveSupport::NumericWithFormat
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
class BigDecimal
|
|
633
|
+
include ActiveSupport::NumericWithFormat
|
|
634
|
+
end
|
|
635
|
+
|
|
607
636
|
# ---------------------------------------------------------------
|
|
608
637
|
# ActiveSupport::Duration — issue #632
|
|
609
638
|
# ---------------------------------------------------------------
|
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
require "rigor/
|
|
5
|
+
require "rigor/reflection"
|
|
6
6
|
|
|
7
7
|
require_relative "resource_index"
|
|
8
8
|
|
|
9
9
|
module Rigor
|
|
10
10
|
module Plugin
|
|
11
11
|
class Alba < Rigor::Plugin::Base
|
|
12
|
-
# A
|
|
13
|
-
# `include Alba::Resource`s, and the association declarations whose resource
|
|
14
|
-
#
|
|
12
|
+
# A collector over one file, built on {Rigor::Reflection.class_declarations_in}: the classes it declares, whether
|
|
13
|
+
# each `include Alba::Resource`s, and the association declarations whose resource class alba infers from
|
|
14
|
+
# the name.
|
|
15
15
|
#
|
|
16
16
|
# An association is recorded only when alba would take the inference path (`Alba::Association#
|
|
17
17
|
# resource_from`): a literal Symbol/String name, exactly one positional argument, no block, and no
|
|
18
18
|
# `resource:` / `serializer:` keyword. A keyword splat (`**opts`) might carry either, so it is not
|
|
19
19
|
# recorded either — a skipped association costs a root, never invents one.
|
|
20
20
|
class ResourceCollector
|
|
21
|
-
include Rigor::Inference::DeclarationWalk::Collector
|
|
22
|
-
|
|
23
21
|
ASSOCIATION_METHODS = %i[association one many has_one has_many].freeze
|
|
24
22
|
RESOURCE_MODULE = "Alba::Resource"
|
|
25
23
|
RESOURCE_KEYWORDS = %i[resource serializer].freeze
|
|
@@ -40,22 +38,13 @@ module Rigor
|
|
|
40
38
|
@associations = []
|
|
41
39
|
end
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@entries[name] = superclass_name(node.superclass)
|
|
49
|
-
@nestings[name] = context.nesting || []
|
|
41
|
+
# Walks `root` (a parsed file's `Prism::Node`) and records what it finds; answers the collector itself.
|
|
42
|
+
def collect(root)
|
|
43
|
+
Rigor::Reflection.class_declarations_in(root) { |call| on_call(call.node, call.owner) }.each do |declaration|
|
|
44
|
+
@entries[declaration.name] = superclass_name(declaration.node.superclass)
|
|
45
|
+
@nestings[declaration.name] = declaration.nesting
|
|
50
46
|
end
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def on_call(node, context)
|
|
55
|
-
owner = context.prefix.join("::")
|
|
56
|
-
record_call(node, owner) unless owner.empty? || !node.receiver.nil?
|
|
57
|
-
remember_block(node)
|
|
58
|
-
Rigor::Inference::DeclarationWalk::DESCEND
|
|
47
|
+
self
|
|
59
48
|
end
|
|
60
49
|
|
|
61
50
|
# The classes this file declares, as {ResourceIndex::ClassEntry} values.
|
|
@@ -70,6 +59,11 @@ module Rigor
|
|
|
70
59
|
|
|
71
60
|
private
|
|
72
61
|
|
|
62
|
+
def on_call(node, owner)
|
|
63
|
+
record_call(node, owner) unless owner.nil? || !node.receiver.nil?
|
|
64
|
+
remember_block(node)
|
|
65
|
+
end
|
|
66
|
+
|
|
73
67
|
def record_call(node, owner)
|
|
74
68
|
if node.name == :include
|
|
75
69
|
@includes[owner] = true if includes_resource?(node)
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
require "rigor/inference/declaration_walk"
|
|
6
|
-
|
|
7
5
|
require_relative "resource_collector"
|
|
8
6
|
require_relative "resource_index"
|
|
9
7
|
|
|
@@ -28,10 +26,7 @@ module Rigor
|
|
|
28
26
|
parsed = Prism.parse(contents)
|
|
29
27
|
next unless parsed.success?
|
|
30
28
|
|
|
31
|
-
collector = ResourceCollector.new
|
|
32
|
-
Rigor::Inference::DeclarationWalk.run(
|
|
33
|
-
parsed.value, [collector], Rigor::Inference::DeclarationWalk::Context.root(nesting: [])
|
|
34
|
-
)
|
|
29
|
+
collector = ResourceCollector.new.collect(parsed.value)
|
|
35
30
|
entries.concat(collector.class_entries)
|
|
36
31
|
associations.concat(collector.associations)
|
|
37
32
|
end
|
|
@@ -47,7 +47,8 @@ module Rigor
|
|
|
47
47
|
catalog = producer_value(:ffi_catalog)
|
|
48
48
|
next nil if catalog.nil?
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
receiver = call_node.receiver
|
|
51
|
+
receiver_type = receiver ? scope&.type_of(receiver) : scope&.self_type
|
|
51
52
|
receiver_class = case receiver_type
|
|
52
53
|
when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
|
|
53
54
|
end
|
|
@@ -77,7 +78,8 @@ module Rigor
|
|
|
77
78
|
catalog = producer_value(:ffi_catalog)
|
|
78
79
|
next nil if catalog.nil?
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
receiver = call_node.receiver
|
|
82
|
+
receiver_type = receiver ? scope&.type_of(receiver) : scope&.self_type
|
|
81
83
|
receiver_class = case receiver_type
|
|
82
84
|
when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
|
|
83
85
|
end
|
|
@@ -77,8 +77,18 @@ module Rigor
|
|
|
77
77
|
signature_paths: ["sig"],
|
|
78
78
|
# ADR-43 WD4 — the classes the bundled sig covers completely enough that a Ruby-source subclass
|
|
79
79
|
# (`class PostType < GraphQL::Schema::Object`) may bridge inherited calls to the RBS ancestor.
|
|
80
|
-
#
|
|
81
|
-
#
|
|
80
|
+
# The bridge is a signature lookup only: an inherited DSL call resolves to the declaration's
|
|
81
|
+
# return type and block parameters, and a subclass `def` overriding a declared method other
|
|
82
|
+
# than `initialize` binds its parameters from it (#2681). The subclass itself stays outside
|
|
83
|
+
# RBS, so no signature-reading call rule fires on a call it makes, declared or not:
|
|
84
|
+
# `call.undefined-method`, `call.wrong-arity` and `call.argument-type-mismatch` all stay
|
|
85
|
+
# silent, while the same calls on a listed class AS THE DIRECT RECEIVER still report.
|
|
86
|
+
# `open_receivers:` is what covers those direct receivers; it is not what silences the
|
|
87
|
+
# subclass — the receiver gate is (ADR-43 Context § "Amendment 2026-11-15", which withdrew
|
|
88
|
+
# the claim that the rules follow the resolution). So listing a class here cannot cost a
|
|
89
|
+
# false positive on a subclass. The risk runs the other way: a declared member that does not exist at runtime makes a genuinely
|
|
90
|
+
# failing call resolve silently, so the sig must be accurate — it IS the authority
|
|
91
|
+
# (graphql-ruby ships none).
|
|
82
92
|
rbs_complete_ancestors: %w[
|
|
83
93
|
GraphQL::Schema
|
|
84
94
|
GraphQL::Schema::Member
|
|
@@ -113,7 +113,13 @@ module Rigor
|
|
|
113
113
|
# ADR-37 slice 2 — a pure return-type contributor. The engine gates on the receiver being a known
|
|
114
114
|
# carrier class (so the block only runs on Result/Option receivers); the block then confirms the call
|
|
115
115
|
# is an unwrap method on an instantiated carrier and yields the carried `type_args[0]`.
|
|
116
|
+
# #2717 — the rule has no `methods:` gate, so the name gate does not screen it: the engine also offers it
|
|
117
|
+
# the compound-write nodes (`x.attr op= v`, `x[i] ||= v`, ...) and the `&:symbol` block's
|
|
118
|
+
# `Prism::BlockArgumentNode`. None of those has `name`, which the rule reads after `receiver`, so decline
|
|
119
|
+
# anything that is not a `CallNode` rather than raise into the rescue.
|
|
116
120
|
dynamic_return receivers: (RESULT_CARRIERS + OPTION_CARRIERS) do |call_node, scope|
|
|
121
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
122
|
+
|
|
117
123
|
receiver = call_node.receiver
|
|
118
124
|
next nil if receiver.nil?
|
|
119
125
|
|
|
@@ -65,7 +65,7 @@ module Rigor
|
|
|
65
65
|
# implicit `*_path` / `*_url` helper call, is shadowed by a same-file binding, or is in a
|
|
66
66
|
# suppressed directory. ADR-37: the engine owns the walk; the same-file `shadowing` set is built
|
|
67
67
|
# once as the node-rule file context.
|
|
68
|
-
def violations_for(call_node:, helper_table:, shadowing:, path:)
|
|
68
|
+
def violations_for(call_node:, helper_table:, shadowing:, path:, scope: nil, context: nil)
|
|
69
69
|
return [] unless call_node.is_a?(Prism::CallNode) && implicit_helper_call?(call_node)
|
|
70
70
|
|
|
71
71
|
name = call_node.name.to_s
|
|
@@ -88,11 +88,209 @@ module Rigor
|
|
|
88
88
|
# Recognised custom / dynamic helper (no arity to check), or a directory where helpers aren't
|
|
89
89
|
# customarily resolved.
|
|
90
90
|
[]
|
|
91
|
+
elsif self_resolves_method?(name, scope, context)
|
|
92
|
+
[]
|
|
91
93
|
else
|
|
92
94
|
[unknown_helper_violation(name, helper_table)]
|
|
93
95
|
end
|
|
94
96
|
end
|
|
95
97
|
|
|
98
|
+
# True when `name` is already a method on the call's `self`, asked of the engine rather than a
|
|
99
|
+
# plugin-side name list: the project's discovered methods (`def`, `attr_*`, the singleton side) through
|
|
100
|
+
# the project's own ancestry, then the RBS ancestry. A truncated walk (`maybe`) declines too — a definer
|
|
101
|
+
# past the budget cannot be ruled out, and a false positive outranks a missed typo.
|
|
102
|
+
#
|
|
103
|
+
# `self` is read from the node's lexical ancestors (`NodeContext#ancestors`), because the scope a
|
|
104
|
+
# `node_rule` receives is the file's, not the call site's. {#call_site} names the enclosing class or
|
|
105
|
+
# module and the kinds `self` may be; where the kind is not decidable (a block in a class body, a
|
|
106
|
+
# `def obj.m`) both are asked, so a definer on either side declines and a typo on neither still reports.
|
|
107
|
+
def self_resolves_method?(name, scope, context)
|
|
108
|
+
return false if scope.nil? || context.nil?
|
|
109
|
+
|
|
110
|
+
site = call_site(context.ancestors)
|
|
111
|
+
return false if site.nil?
|
|
112
|
+
# An owner no constant spelling names (`class foo::Bar`) cannot be asked about.
|
|
113
|
+
return true if site.names.empty?
|
|
114
|
+
# A module is mixed into classes that cannot be seen from where it is defined (a controller concern).
|
|
115
|
+
return true if site.module_owner && FRAMEWORK_METHODS.key?(name)
|
|
116
|
+
|
|
117
|
+
site.names.product(site.kinds).any? { |class_name, kind| resolves_on?(class_name, name, kind, scope) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def resolves_on?(class_name, name, kind, scope)
|
|
121
|
+
return true unless Rigor::Reflection.project_method_defined(class_name, name, kind: kind, source: :either,
|
|
122
|
+
scope: scope).no?
|
|
123
|
+
return true unless Rigor::Reflection.rbs_method_defined(class_name, name, kind: kind, scope: scope).no?
|
|
124
|
+
|
|
125
|
+
bases = FRAMEWORK_METHODS[name]
|
|
126
|
+
!bases.nil? && !Rigor::Reflection.ancestry_reaches(class_name, bases, kind: kind, scope: scope).no?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Rails' own `*_path` / `*_url` methods on a controller or mailer, read from the Rails source (rails/rails
|
|
130
|
+
# main, 1105756f42), each with the framework classes a project class names directly as its superclass. A
|
|
131
|
+
# project without that framework's RBS cannot see these through the ancestry, so a class whose ancestry
|
|
132
|
+
# reaches one of the listed bases declines for that name; a module declines for every name, being mixed
|
|
133
|
+
# into classes that cannot be seen.
|
|
134
|
+
#
|
|
135
|
+
# - `controller_path`: AbstractController::Base, actionpack/lib/abstract_controller/base.rb:157 (instance),
|
|
136
|
+
# :117 (class); every framework base below inherits it (metal.rb:185, base.rb:206, api.rb:91,
|
|
137
|
+
# actionmailer/lib/action_mailer/base.rb:479).
|
|
138
|
+
# - `append_view_path` / `prepend_view_path`: `ActionView::ViewPaths` (actionview/lib/action_view/
|
|
139
|
+
# view_paths.rb:113, :124 instance, :46, :57 class), which `AbstractController::Rendering` includes
|
|
140
|
+
# (abstract_controller/rendering.rb:18); `ActionController::Base` (base.rb:270), `::API` (api.rb:115) and
|
|
141
|
+
# `ActionMailer::Base` (action_mailer/base.rb:489) include that.
|
|
142
|
+
# - `helpers_path`: a class attribute of `ActionController::Helpers` (metal/helpers.rb:68), included by
|
|
143
|
+
# `ActionController::Base` (base.rb:273).
|
|
144
|
+
# - `edit_` / `new_` `polymorphic_path` / `polymorphic_url`: actionpack/lib/action_dispatch/routing/
|
|
145
|
+
# polymorphic_routes.rb:163-166, through `UrlFor` in `ActionController::Base` (base.rb:274) and `::API`
|
|
146
|
+
# (api.rb:117), and in `ActionMailer::Base`, which actionmailer/lib/action_mailer/railtie.rb:40
|
|
147
|
+
# includes `AbstractController::UrlFor` into, whose `ActionDispatch::Routing::UrlFor` includes
|
|
148
|
+
# `PolymorphicRoutes` (action_dispatch/routing/url_for.rb:92). (`polymorphic_path` / `polymorphic_url` are in {BUILTIN_PASSTHROUGH}.)
|
|
149
|
+
FRAMEWORK_CONTROLLERS = %w[
|
|
150
|
+
AbstractController::Base ActionController::Metal ActionController::Base ActionController::API
|
|
151
|
+
ActionMailer::Base
|
|
152
|
+
].freeze
|
|
153
|
+
VIEW_PATH_BASES = %w[ActionController::Base ActionController::API ActionMailer::Base].freeze
|
|
154
|
+
URL_FOR_BASES = %w[ActionController::Base ActionController::API ActionMailer::Base].freeze
|
|
155
|
+
FRAMEWORK_METHODS = {
|
|
156
|
+
"controller_path" => FRAMEWORK_CONTROLLERS,
|
|
157
|
+
"append_view_path" => VIEW_PATH_BASES,
|
|
158
|
+
"prepend_view_path" => VIEW_PATH_BASES,
|
|
159
|
+
"helpers_path" => %w[ActionController::Base].freeze,
|
|
160
|
+
"edit_polymorphic_path" => URL_FOR_BASES,
|
|
161
|
+
"edit_polymorphic_url" => URL_FOR_BASES,
|
|
162
|
+
"new_polymorphic_path" => URL_FOR_BASES,
|
|
163
|
+
"new_polymorphic_url" => URL_FOR_BASES
|
|
164
|
+
}.freeze
|
|
165
|
+
|
|
166
|
+
# What a call site's lexical position says about `self`: the names the enclosing class or module can be
|
|
167
|
+
# known by, whether the owner is a module, and the kinds (`:instance`, `:singleton`) `self` may be.
|
|
168
|
+
CallSite = Struct.new(:names, :module_owner, :kinds, keyword_init: true)
|
|
169
|
+
|
|
170
|
+
# The {CallSite} of a node under `ancestors` (outermost first), or nil when it is in no class or module,
|
|
171
|
+
# which leaves the route-table verdict as it was.
|
|
172
|
+
def call_site(ancestors)
|
|
173
|
+
owner_index = ancestors.rindex { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
174
|
+
member_site = member_class_site(ancestors, owner_index)
|
|
175
|
+
return member_site if member_site
|
|
176
|
+
return nil if owner_index.nil?
|
|
177
|
+
|
|
178
|
+
CallSite.new(names: owner_names(ancestors, owner_index),
|
|
179
|
+
module_owner: ancestors[owner_index].is_a?(Prism::ModuleNode),
|
|
180
|
+
kinds: self_kinds(ancestors[(owner_index + 1)..]))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
BOTH_KINDS = %i[instance singleton].freeze
|
|
184
|
+
MEMBER_CLASS_CONSTRUCTORS = { Struct: :new, Data: :define }.freeze
|
|
185
|
+
|
|
186
|
+
# The call site of a node inside the block of `Struct.new(...) { }` / `Data.define(...) { }`, which is the
|
|
187
|
+
# body of a class the constructor creates: `self` is that class, not the lexical `class` around it (or
|
|
188
|
+
# none). A plain constant write (`Point = …`, `Outer::Point = …`, nested under the lexical owner) names it,
|
|
189
|
+
# and the engine knows its members. Everything else declines (`names` empty): an unassigned block, `P ||= …`,
|
|
190
|
+
# `… end.freeze`, a dynamic path. nil when the node is not in such a block below its lexical owner.
|
|
191
|
+
def member_class_site(ancestors, owner_index)
|
|
192
|
+
index = ancestors.rindex { |n| member_class_call?(n) && ancestors[ancestors.index(n) + 1].equal?(n.block) }
|
|
193
|
+
return nil if index.nil? || (owner_index && index < owner_index)
|
|
194
|
+
|
|
195
|
+
kinds = self_kinds(ancestors[(index + 1)..])
|
|
196
|
+
names = index.positive? ? written_class_names(ancestors, owner_index, ancestors[index - 1], ancestors[index]) : []
|
|
197
|
+
names = struct_class_names(ancestors[index]) if names.empty?
|
|
198
|
+
CallSite.new(names: names, module_owner: false, kinds: kinds)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Issue #2968 — `Struct.new("Named", :a) { }` names its class `Struct::Named` without a constant write.
|
|
202
|
+
def struct_class_names(call)
|
|
203
|
+
first = call.arguments&.arguments&.first
|
|
204
|
+
return [] unless call.name == :new && first.is_a?(Prism::StringNode)
|
|
205
|
+
|
|
206
|
+
name = first.unescaped
|
|
207
|
+
name.match?(/\A[A-Z][A-Za-z0-9_]*\z/) ? ["Struct::#{name}"] : []
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def written_class_names(ancestors, owner_index, written, call)
|
|
211
|
+
local =
|
|
212
|
+
case written
|
|
213
|
+
when Prism::ConstantWriteNode then written.value.equal?(call) ? written.name.to_s : nil
|
|
214
|
+
when Prism::ConstantPathWriteNode then written.value.equal?(call) ? declared_name(written.target) : nil
|
|
215
|
+
end
|
|
216
|
+
return [] if local.nil?
|
|
217
|
+
return [local.delete_prefix("::")] if local.start_with?("::")
|
|
218
|
+
|
|
219
|
+
prefixes = owner_index ? owner_names(ancestors, owner_index) : [""]
|
|
220
|
+
nested = prefixes.map { |prefix| prefix.empty? ? local : "#{prefix}::#{local}" }
|
|
221
|
+
# `Outer::R = …` resolves `Outer` lexically or at the root, as a compact `class Foo::Bar` does.
|
|
222
|
+
written.is_a?(Prism::ConstantPathWriteNode) ? (nested + [local]).uniq : nested
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def member_class_call?(node)
|
|
226
|
+
return false unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
|
|
227
|
+
|
|
228
|
+
receiver = node.receiver
|
|
229
|
+
receiver_name =
|
|
230
|
+
case receiver
|
|
231
|
+
when Prism::ConstantReadNode then receiver.name
|
|
232
|
+
when Prism::ConstantPathNode then receiver.parent.nil? ? receiver.name : nil
|
|
233
|
+
end
|
|
234
|
+
!receiver_name.nil? && MEMBER_CLASS_CONSTRUCTORS[receiver_name] == node.name
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The kinds `self` may be for a node under the nodes between its owner and itself. The nearest `def`
|
|
238
|
+
# decides (blocks inside a method keep its `self`); with no `def`, a bare class body is the class object
|
|
239
|
+
# itself, while a block or lambda there may run on another object (`before_action { }`, `included do`,
|
|
240
|
+
# `define_method`, `Class.new`), so both kinds are asked.
|
|
241
|
+
def self_kinds(inner)
|
|
242
|
+
def_index = inner.rindex { |n| n.is_a?(Prism::DefNode) }
|
|
243
|
+
if def_index
|
|
244
|
+
node = inner[def_index]
|
|
245
|
+
return BOTH_KINDS if node.receiver && !node.receiver.is_a?(Prism::SelfNode)
|
|
246
|
+
return [:singleton] if node.receiver
|
|
247
|
+
|
|
248
|
+
return inner[0...def_index].any?(Prism::SingletonClassNode) ? [:singleton] : [:instance]
|
|
249
|
+
end
|
|
250
|
+
return BOTH_KINDS if inner.any? { |n| block_scope?(n) }
|
|
251
|
+
if inner.any? { |n| n.is_a?(Prism::SingletonClassNode) && !n.expression.is_a?(Prism::SelfNode) }
|
|
252
|
+
return BOTH_KINDS
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
[:singleton]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def block_scope?(node)
|
|
259
|
+
node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The names the enclosing class or module can be known by, `[]` when a declaration's name is dynamic. A
|
|
263
|
+
# bare `class Foo` is exactly `Enclosing::Foo`. A compact `class Foo::Bar` may resolve `Foo` lexically or
|
|
264
|
+
# at the root, so both spellings are tried. A rooted declaration (`class ::Foo`) names the root constant
|
|
265
|
+
# alone.
|
|
266
|
+
def owner_names(ancestors, owner_index)
|
|
267
|
+
chain = ancestors[0..owner_index].select { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
268
|
+
names = [""]
|
|
269
|
+
chain.each do |decl|
|
|
270
|
+
local = declared_name(decl.constant_path)
|
|
271
|
+
return [] if local.nil?
|
|
272
|
+
|
|
273
|
+
names = if local.start_with?("::")
|
|
274
|
+
[local.delete_prefix("::")]
|
|
275
|
+
else
|
|
276
|
+
nested = names.map { |prefix| prefix.empty? ? local : "#{prefix}::#{local}" }
|
|
277
|
+
decl.constant_path.is_a?(Prism::ConstantPathNode) ? (nested + [local]).uniq : nested
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
names
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def declared_name(node)
|
|
284
|
+
case node
|
|
285
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
286
|
+
when Prism::ConstantPathNode
|
|
287
|
+
return "::#{node.name}" if node.parent.nil?
|
|
288
|
+
|
|
289
|
+
parent = declared_name(node.parent)
|
|
290
|
+
parent && "#{parent}::#{node.name}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
96
294
|
# Walks the AST once and returns the Set of names that shadow a route helper for this file. Includes:
|
|
97
295
|
#
|
|
98
296
|
# - `def name` declarations (any level — the per-method-scope visibility model Ruby uses means a
|
|
@@ -205,11 +205,12 @@ module Rigor
|
|
|
205
205
|
Analyzer.collect_shadowing_names(root)
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
-
node_rule Prism::CallNode do |node,
|
|
208
|
+
node_rule Prism::CallNode do |node, scope, path, shadowing, context|
|
|
209
209
|
table = helper_table_or_nil
|
|
210
210
|
next [] if table.nil? || table.empty?
|
|
211
211
|
|
|
212
|
-
Analyzer.violations_for(call_node: node, helper_table: table, shadowing: shadowing, path: path
|
|
212
|
+
Analyzer.violations_for(call_node: node, helper_table: table, shadowing: shadowing, path: path,
|
|
213
|
+
scope: scope, context: context).map do |violation|
|
|
213
214
|
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
214
215
|
end
|
|
215
216
|
end
|