rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -8,12 +8,18 @@ require_relative "../source/constant_path"
|
|
|
8
8
|
require_relative "../type"
|
|
9
9
|
require_relative "diagnostic"
|
|
10
10
|
require_relative "dependency_recorder"
|
|
11
|
+
require_relative "check_rules/rule_ids"
|
|
12
|
+
require_relative "check_rules/inferred_param_guard"
|
|
11
13
|
require_relative "check_rules/rule_walk"
|
|
12
14
|
require_relative "check_rules/always_truthy_condition_collector"
|
|
13
15
|
require_relative "check_rules/unreachable_clause_collector"
|
|
16
|
+
require_relative "check_rules/shadowed_rescue_collector"
|
|
14
17
|
require_relative "check_rules/dead_assignment_collector"
|
|
18
|
+
require_relative "check_rules/duplicate_hash_key_collector"
|
|
19
|
+
require_relative "check_rules/return_in_ensure_collector"
|
|
15
20
|
require_relative "check_rules/ivar_write_collector"
|
|
16
21
|
require_relative "check_rules/main_pass_collector"
|
|
22
|
+
require_relative "check_rules/void_value_use_collector"
|
|
17
23
|
require_relative "check_rules/self_closedness_scanner"
|
|
18
24
|
|
|
19
25
|
module Rigor
|
|
@@ -44,88 +50,8 @@ module Rigor
|
|
|
44
50
|
# loader (RBS-blind environments, unknown stdlib).
|
|
45
51
|
# rubocop:disable Metrics/ModuleLength
|
|
46
52
|
module CheckRules
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# `family.rule-name` two-segment strings; the families
|
|
50
|
-
# group diagnostics by where they originate
|
|
51
|
-
# (`call.*` for call-site rules, `flow.*` for flow-analysis
|
|
52
|
-
# proofs, `assert.*` for runtime-assertion rules,
|
|
53
|
-
# `dump.*` for debug helpers, `def.*` for method-definition
|
|
54
|
-
# rules). Used by the configuration `disable:` list and the
|
|
55
|
-
# in-source `# rigor:disable <rule>` suppression comment
|
|
56
|
-
# system; new rules MUST register here so user configuration
|
|
57
|
-
# can refer to them.
|
|
58
|
-
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
59
|
-
RULE_SELF_UNDEFINED_METHOD = "call.self-undefined-method"
|
|
60
|
-
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
61
|
-
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
62
|
-
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
63
|
-
RULE_NIL_RECEIVER = "call.possible-nil-receiver"
|
|
64
|
-
RULE_DUMP_TYPE = "dump.type"
|
|
65
|
-
RULE_ASSERT_TYPE = "assert.type-mismatch"
|
|
66
|
-
RULE_ALWAYS_RAISES = "flow.always-raises"
|
|
67
|
-
RULE_UNREACHABLE_BRANCH = "flow.unreachable-branch"
|
|
68
|
-
RULE_RETURN_TYPE = "def.return-type-mismatch"
|
|
69
|
-
RULE_VISIBILITY_MISMATCH = "def.method-visibility-mismatch"
|
|
70
|
-
RULE_OVERRIDE_VISIBILITY_REDUCED = "def.override-visibility-reduced"
|
|
71
|
-
RULE_OVERRIDE_RETURN_WIDENED = "def.override-return-widened"
|
|
72
|
-
RULE_OVERRIDE_PARAM_NARROWED = "def.override-param-narrowed"
|
|
73
|
-
RULE_IVAR_WRITE_MISMATCH = "def.ivar-write-mismatch"
|
|
74
|
-
RULE_DEAD_ASSIGNMENT = "flow.dead-assignment"
|
|
75
|
-
RULE_ALWAYS_TRUTHY_CONDITION = "flow.always-truthy-condition"
|
|
76
|
-
RULE_UNREACHABLE_CLAUSE = "flow.unreachable-clause"
|
|
77
|
-
|
|
78
|
-
ALL_RULES = [
|
|
79
|
-
RULE_UNDEFINED_METHOD,
|
|
80
|
-
RULE_SELF_UNDEFINED_METHOD,
|
|
81
|
-
RULE_UNRESOLVED_TOPLEVEL,
|
|
82
|
-
RULE_WRONG_ARITY,
|
|
83
|
-
RULE_ARGUMENT_TYPE,
|
|
84
|
-
RULE_NIL_RECEIVER,
|
|
85
|
-
RULE_DUMP_TYPE,
|
|
86
|
-
RULE_ASSERT_TYPE,
|
|
87
|
-
RULE_ALWAYS_RAISES,
|
|
88
|
-
RULE_UNREACHABLE_BRANCH,
|
|
89
|
-
RULE_DEAD_ASSIGNMENT,
|
|
90
|
-
RULE_ALWAYS_TRUTHY_CONDITION,
|
|
91
|
-
RULE_UNREACHABLE_CLAUSE,
|
|
92
|
-
RULE_RETURN_TYPE,
|
|
93
|
-
RULE_VISIBILITY_MISMATCH,
|
|
94
|
-
RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
95
|
-
RULE_OVERRIDE_RETURN_WIDENED,
|
|
96
|
-
RULE_OVERRIDE_PARAM_NARROWED,
|
|
97
|
-
RULE_IVAR_WRITE_MISMATCH
|
|
98
|
-
].freeze
|
|
99
|
-
|
|
100
|
-
# Backward-compat alias table (ADR-8 § "Backward
|
|
101
|
-
# compatibility"). Existing user code with
|
|
102
|
-
# `# rigor:disable undefined-method` /
|
|
103
|
-
# `disable: [undefined-method]` keeps working — the
|
|
104
|
-
# legacy unprefixed identifiers map to their canonical
|
|
105
|
-
# `family.rule-name` form here. Removing the aliases is
|
|
106
|
-
# a future ADR once user code has migrated; until then,
|
|
107
|
-
# both spellings resolve identically.
|
|
108
|
-
LEGACY_RULE_ALIASES = {
|
|
109
|
-
"undefined-method" => RULE_UNDEFINED_METHOD,
|
|
110
|
-
"self-undefined-method" => RULE_SELF_UNDEFINED_METHOD,
|
|
111
|
-
"wrong-arity" => RULE_WRONG_ARITY,
|
|
112
|
-
"argument-type-mismatch" => RULE_ARGUMENT_TYPE,
|
|
113
|
-
"possible-nil-receiver" => RULE_NIL_RECEIVER,
|
|
114
|
-
"dump-type" => RULE_DUMP_TYPE,
|
|
115
|
-
"assert-type" => RULE_ASSERT_TYPE,
|
|
116
|
-
"always-raises" => RULE_ALWAYS_RAISES,
|
|
117
|
-
"unreachable-branch" => RULE_UNREACHABLE_BRANCH,
|
|
118
|
-
"method-visibility-mismatch" => RULE_VISIBILITY_MISMATCH,
|
|
119
|
-
"ivar-write-mismatch" => RULE_IVAR_WRITE_MISMATCH,
|
|
120
|
-
"dead-assignment" => RULE_DEAD_ASSIGNMENT,
|
|
121
|
-
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION,
|
|
122
|
-
"unreachable-clause" => RULE_UNREACHABLE_CLAUSE
|
|
123
|
-
}.freeze
|
|
124
|
-
|
|
125
|
-
# Family wildcard — a `<family>` token in a suppression
|
|
126
|
-
# comment or `disable:` list disables every rule whose
|
|
127
|
-
# canonical id starts with `<family>.`. Per ADR-8 § "1".
|
|
128
|
-
RULE_FAMILIES = %w[call flow assert dump def].freeze
|
|
53
|
+
# ADR-87 WD4 — the canonical rule-id table lives in the light `check_rules/rule_ids.rb` (required at the
|
|
54
|
+
# top of this file) so {Analysis::RuleCatalog} can read it without loading the engine.
|
|
129
55
|
|
|
130
56
|
# ADR-35 slice 1 — bound for the `def.override-visibility-reduced`
|
|
131
57
|
# ancestor walk, and the public > protected > private ordering
|
|
@@ -171,13 +97,34 @@ module Rigor
|
|
|
171
97
|
collectors = node_collectors || run_node_collectors(path, root, scope_index)
|
|
172
98
|
diagnostics = collectors[:main_pass].results.dup
|
|
173
99
|
diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
100
|
+
COLLECTOR_DIAGNOSTIC_BUILDERS.each do |role, builder|
|
|
101
|
+
diagnostics.concat(send(builder, path, collectors[role].results))
|
|
102
|
+
end
|
|
103
|
+
# Suppression-marker validation (`suppression.*`) runs BEFORE the filter so its own diagnostics are
|
|
104
|
+
# suppressible like any other rule — `# rigor:disable suppression.unknown-rule` on the offending
|
|
105
|
+
# comment's line works, with no regress (the token itself is known, so it never re-fires).
|
|
106
|
+
diagnostics.concat(suppression_marker_diagnostics(path, comments))
|
|
178
107
|
filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules)
|
|
179
108
|
end
|
|
180
109
|
|
|
110
|
+
# The per-collector diagnostic builders {.diagnose} folds over, in the historical emission order
|
|
111
|
+
# (value-use-void → always-truthy → unreachable-clause → shadowed-rescue → ivar-write →
|
|
112
|
+
# dead-assignment → duplicate-hash-key → return-in-ensure). Keys match {.build_node_collectors}'
|
|
113
|
+
# roles; each value is a `(path, results)` module_function on this module. `void_value_use` leads
|
|
114
|
+
# because its standalone walk used to run before this fold, and the emission order is byte-identical
|
|
115
|
+
# output, not an implementation detail.
|
|
116
|
+
COLLECTOR_DIAGNOSTIC_BUILDERS = {
|
|
117
|
+
void_value_use: :void_value_use_diagnostics,
|
|
118
|
+
always_truthy: :always_truthy_condition_diagnostics,
|
|
119
|
+
unreachable_clauses: :unreachable_clause_diagnostics,
|
|
120
|
+
shadowed_rescues: :shadowed_rescue_diagnostics,
|
|
121
|
+
ivar_writes: :ivar_write_mismatch_diagnostics,
|
|
122
|
+
dead_assignments: :dead_assignment_diagnostics,
|
|
123
|
+
duplicate_hash_keys: :duplicate_hash_key_diagnostics,
|
|
124
|
+
return_in_ensure: :return_in_ensure_diagnostics
|
|
125
|
+
}.freeze
|
|
126
|
+
private_constant :COLLECTOR_DIAGNOSTIC_BUILDERS
|
|
127
|
+
|
|
181
128
|
# The verbatim per-node dispatch of the former inline main pass
|
|
182
129
|
# (`diagnose`'s `Source::NodeWalker.each` `case`), now invoked by
|
|
183
130
|
# {MainPassCollector} on the shared {RuleWalk}. Returns the
|
|
@@ -210,10 +157,14 @@ module Rigor
|
|
|
210
157
|
def build_node_collectors(path, scope_index)
|
|
211
158
|
{
|
|
212
159
|
main_pass: MainPassCollector.new(->(node) { main_pass_node_diagnostics(path, node, scope_index) }),
|
|
160
|
+
void_value_use: VoidValueUseCollector.new(scope_index),
|
|
213
161
|
always_truthy: AlwaysTruthyConditionCollector.new(scope_index),
|
|
214
162
|
unreachable_clauses: UnreachableClauseCollector.new(scope_index),
|
|
163
|
+
shadowed_rescues: ShadowedRescueCollector.new(scope_index),
|
|
215
164
|
ivar_writes: IvarWriteCollector.new(scope_index),
|
|
216
|
-
dead_assignments: DeadAssignmentCollector.new(scope_index)
|
|
165
|
+
dead_assignments: DeadAssignmentCollector.new(scope_index),
|
|
166
|
+
duplicate_hash_keys: DuplicateHashKeyCollector.new(scope_index),
|
|
167
|
+
return_in_ensure: ReturnInEnsureCollector.new(scope_index)
|
|
217
168
|
}
|
|
218
169
|
end
|
|
219
170
|
|
|
@@ -313,6 +264,7 @@ module Rigor
|
|
|
313
264
|
dump_type_diagnostic(path, node, scope_index),
|
|
314
265
|
assert_type_diagnostic(path, node, scope_index),
|
|
315
266
|
always_raises_diagnostic(path, node, scope_index),
|
|
267
|
+
raise_non_exception_diagnostic(path, node, scope_index),
|
|
316
268
|
visibility_mismatch_diagnostic(path, node, scope_index)
|
|
317
269
|
].compact
|
|
318
270
|
end
|
|
@@ -357,6 +309,31 @@ module Rigor
|
|
|
357
309
|
end
|
|
358
310
|
end
|
|
359
311
|
|
|
312
|
+
# v0.3.0 — `flow.duplicate-hash-key`. Emits a diagnostic for each LATER occurrence of a repeated
|
|
313
|
+
# LITERAL key within one Hash literal (braced or bare-kwargs) — Ruby keeps the last entry silently
|
|
314
|
+
# at runtime, so the earlier value is dead. The
|
|
315
|
+
# `Analysis::CheckRules::DuplicateHashKeyCollector` describes the value-pinned-literal-only
|
|
316
|
+
# envelope (symbols / plain strings / integers / floats / true / false / nil; never
|
|
317
|
+
# cross-kind, never interpolation / constants / calls / splats).
|
|
318
|
+
def duplicate_hash_key_diagnostics(path, duplicate_keys)
|
|
319
|
+
duplicate_keys.map do |result|
|
|
320
|
+
build_duplicate_hash_key_diagnostic(path, result)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# v0.3.0 — `flow.return-in-ensure`. One diagnostic per explicit
|
|
325
|
+
# `return` lexically inside an `ensure` clause body: it silently
|
|
326
|
+
# discards the method's in-flight return value and swallows any
|
|
327
|
+
# in-flight exception. Purely syntactic; the
|
|
328
|
+
# `Analysis::CheckRules::ReturnInEnsureCollector` describes the
|
|
329
|
+
# frame-aware envelope (nested def / lambda / `define_method`
|
|
330
|
+
# blocks are excluded, plain blocks are not).
|
|
331
|
+
def return_in_ensure_diagnostics(path, results)
|
|
332
|
+
results.map do |result|
|
|
333
|
+
build_return_in_ensure_diagnostic(path, result[:return_node])
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
360
337
|
# v0.1.2 — `flow.always-truthy-condition`. Fires on
|
|
361
338
|
# `if` / `unless` / ternary predicates whose inferred
|
|
362
339
|
# type is a `Type::Constant` AND that don't fall in
|
|
@@ -380,6 +357,24 @@ module Rigor
|
|
|
380
357
|
end
|
|
381
358
|
end
|
|
382
359
|
|
|
360
|
+
# ADR-100 WD2 — `static.value-use.void`. Its value-context slot inspection rides the shared per-node
|
|
361
|
+
# {RuleWalk} like the other collectors, so the file is not re-traversed for it. Each result is a
|
|
362
|
+
# value-context use of a call whose author-declared `-> void` return the engine recovered to `top`.
|
|
363
|
+
def void_value_use_diagnostics(path, results)
|
|
364
|
+
results.map do |result|
|
|
365
|
+
build_void_value_use_diagnostic(path, result)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# `flow.shadowed-rescue-clause` — one diagnostic per rescue clause every earlier-comparable class of
|
|
370
|
+
# which is already caught by an earlier clause of the same chain (see {ShadowedRescueCollector} for
|
|
371
|
+
# the ancestry-certainty envelope).
|
|
372
|
+
def shadowed_rescue_diagnostics(path, results)
|
|
373
|
+
results.map do |result|
|
|
374
|
+
build_shadowed_rescue_diagnostic(path, result)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
383
378
|
def ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes)
|
|
384
379
|
return [] if writes.size < 2
|
|
385
380
|
|
|
@@ -454,6 +449,23 @@ module Rigor
|
|
|
454
449
|
FILE_SUPPRESSION_PATTERN = /#\s*rigor:disable-file\s+(?<rules>[\w.,\s-]+)/
|
|
455
450
|
private_constant :FILE_SUPPRESSION_PATTERN
|
|
456
451
|
|
|
452
|
+
# A `rigor:disable[-file]` marker word regardless of whether any rule tokens follow. Used only by the
|
|
453
|
+
# `suppression.empty` detection — the two suppression patterns above require at least one token
|
|
454
|
+
# character, so a bare `# rigor:disable` never reaches them. The lookahead keeps
|
|
455
|
+
# `rigor:disable-something-else` from counting as a marker.
|
|
456
|
+
BARE_SUPPRESSION_MARKER = /#\s*rigor:disable(?<file>-file)?(?![\w-])(?<rest>.*)/
|
|
457
|
+
private_constant :BARE_SUPPRESSION_MARKER
|
|
458
|
+
|
|
459
|
+
# A `rigor:` marker word that is NOT part of Rigor's suppression grammar but reads like an attempted
|
|
460
|
+
# suppression — the RuboCop-reflex spellings `rigor:disable-next-line <rules>` and
|
|
461
|
+
# `rigor:enable <rules>`. These are invisible to both suppression patterns above (the hyphenated
|
|
462
|
+
# suffix fails LINE_SUPPRESSION_PATTERN's `\s+` and BARE_SUPPRESSION_MARKER's lookahead), so without
|
|
463
|
+
# surveillance they silently suppress nothing. Matches `disable-<suffix>` for any suffix other than
|
|
464
|
+
# `file`, and `enable` with or without a suffix.
|
|
465
|
+
UNKNOWN_SUPPRESSION_MARKER =
|
|
466
|
+
/#\s*rigor:(?<marker>disable-(?!file(?![\w-]))[\w-]+|enable(?:-[\w-]+)?)(?![\w-])(?<rest>.*)/
|
|
467
|
+
private_constant :UNKNOWN_SUPPRESSION_MARKER
|
|
468
|
+
|
|
457
469
|
# @return [Array<(Hash{Integer => Set}, Set)>] pair of
|
|
458
470
|
# `(line_suppressions, file_suppressions)`. Line
|
|
459
471
|
# suppressions are keyed by source line number; file
|
|
@@ -478,6 +490,128 @@ module Rigor
|
|
|
478
490
|
end
|
|
479
491
|
end
|
|
480
492
|
|
|
493
|
+
# PHPStan-`IgnoreParseErrorRule`-modelled surveillance over the suppression markers themselves: a
|
|
494
|
+
# malformed or ineffective `# rigor:disable` / `# rigor:disable-file` comment must not silently no-op
|
|
495
|
+
# (the typo'd `# rigor:disable call.undefined-metod` suppresses nothing and the user never learns).
|
|
496
|
+
# Emits `suppression.unknown-rule` for every marker token that resolves to no known identifier, and
|
|
497
|
+
# `suppression.empty` for a marker that lists no rules at all. The MATCHING semantics are deliberately
|
|
498
|
+
# unchanged — an unknown token is still kept verbatim per the diagnostic-policy spec — so this is
|
|
499
|
+
# additive surveillance only. Both diagnostics run before {.filter_suppressed} and flow through it,
|
|
500
|
+
# so they are themselves suppressible (`# rigor:disable suppression.unknown-rule`) with no regress:
|
|
501
|
+
# that token is known, so acknowledging it never re-fires the rule.
|
|
502
|
+
def suppression_marker_diagnostics(path, comments)
|
|
503
|
+
comments.each_with_object([]) do |comment, diagnostics|
|
|
504
|
+
source = comment.location.slice
|
|
505
|
+
if (match = FILE_SUPPRESSION_PATTERN.match(source))
|
|
506
|
+
validate_suppression_tokens(match[:rules], "rigor:disable-file", path, comment, diagnostics)
|
|
507
|
+
elsif (match = LINE_SUPPRESSION_PATTERN.match(source))
|
|
508
|
+
validate_suppression_tokens(match[:rules], "rigor:disable", path, comment, diagnostics)
|
|
509
|
+
else
|
|
510
|
+
diagnose_bare_suppression_marker(path, comment, source, diagnostics)
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def validate_suppression_tokens(raw, marker, path, comment, diagnostics)
|
|
516
|
+
tokens = raw.to_s.split(/[\s,]+/).reject(&:empty?)
|
|
517
|
+
if tokens.empty?
|
|
518
|
+
diagnostics << empty_suppression_diagnostic(path, comment, marker)
|
|
519
|
+
return
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
tokens.each do |token|
|
|
523
|
+
next if known_suppression_token?(token)
|
|
524
|
+
|
|
525
|
+
diagnostics << unknown_suppression_rule_diagnostic(path, comment, marker, token)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# A comment carrying the marker word but not the token-bearing suppression grammar. A remainder of
|
|
530
|
+
# nothing but whitespace / commas is a genuinely empty marker (`# rigor:disable`); anything else
|
|
531
|
+
# (documentation prose like "`# rigor:disable <rule>` comments") is left alone as an ordinary
|
|
532
|
+
# comment, matching the parse path, which never treats it as a suppression either.
|
|
533
|
+
def diagnose_bare_suppression_marker(path, comment, source, diagnostics)
|
|
534
|
+
bare = BARE_SUPPRESSION_MARKER.match(source)
|
|
535
|
+
if bare
|
|
536
|
+
return unless bare[:rest].match?(/\A[\s,]*\z/)
|
|
537
|
+
|
|
538
|
+
marker = bare[:file] ? "rigor:disable-file" : "rigor:disable"
|
|
539
|
+
diagnostics << empty_suppression_diagnostic(path, comment, marker)
|
|
540
|
+
return
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
diagnose_unknown_suppression_marker(path, comment, source, diagnostics)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# `# rigor:disable-next-line <rule>` / `# rigor:enable <rule>` — a marker word Rigor's grammar does
|
|
547
|
+
# not recognise but that reads as an attempted suppression (the RuboCop reflex). Fires only when the
|
|
548
|
+
# remainder is empty or looks like a rule list, so prose mentioning the spelling in backticks stays
|
|
549
|
+
# an ordinary comment — the same escape the empty-marker detection observes.
|
|
550
|
+
def diagnose_unknown_suppression_marker(path, comment, source, diagnostics)
|
|
551
|
+
unknown = UNKNOWN_SUPPRESSION_MARKER.match(source)
|
|
552
|
+
return if unknown.nil?
|
|
553
|
+
|
|
554
|
+
rest = unknown[:rest]
|
|
555
|
+
return unless rest.match?(/\A[\s,]*\z/) || rest.match?(/\A\s+[\w.,\s-]+\z/)
|
|
556
|
+
|
|
557
|
+
diagnostics << unknown_suppression_marker_diagnostic(path, comment, unknown[:marker])
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# True when a suppression token resolves to a diagnostic identifier some producer can emit: a
|
|
561
|
+
# canonical CheckRules id, a legacy alias, the `all` wildcard, a family wildcard, a bare
|
|
562
|
+
# non-catalogue engine id, or a dotted id under a known non-check family (`rbs_extended.*`,
|
|
563
|
+
# `dynamic.*`, `rbs.*`, `pre-eval.*`, and any `plugin.`-prefixed id — plugins load dynamically, so
|
|
564
|
+
# their rule vocabulary cannot be enumerated here and under-warning is the FP-safe direction).
|
|
565
|
+
def known_suppression_token?(token)
|
|
566
|
+
return true if token == "all"
|
|
567
|
+
return true if ALL_RULES.include?(token) || LEGACY_RULE_ALIASES.key?(token) ||
|
|
568
|
+
RULE_FAMILIES.include?(token) || NON_CHECK_DIAGNOSTIC_IDS.include?(token)
|
|
569
|
+
|
|
570
|
+
family, rest = token.split(".", 2)
|
|
571
|
+
!rest.nil? && NON_CHECK_DIAGNOSTIC_FAMILIES.include?(family)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def unknown_suppression_rule_diagnostic(path, comment, marker, token)
|
|
575
|
+
Diagnostic.new(
|
|
576
|
+
path: path,
|
|
577
|
+
line: comment.location.start_line,
|
|
578
|
+
column: comment.location.start_column + 1,
|
|
579
|
+
message: "unknown rule `#{token}` in `# #{marker}` — the token matches no known rule, alias, " \
|
|
580
|
+
"or family, so this suppression has no effect. Likely a typo; `rigor explain <rule>` " \
|
|
581
|
+
"lists the canonical ids.",
|
|
582
|
+
severity: :warning,
|
|
583
|
+
rule: RULE_SUPPRESSION_UNKNOWN_RULE,
|
|
584
|
+
source_family: :builtin
|
|
585
|
+
)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def unknown_suppression_marker_diagnostic(path, comment, marker)
|
|
589
|
+
Diagnostic.new(
|
|
590
|
+
path: path,
|
|
591
|
+
line: comment.location.start_line,
|
|
592
|
+
column: comment.location.start_column + 1,
|
|
593
|
+
message: "unrecognised suppression marker `rigor:#{marker}` — Rigor's markers are " \
|
|
594
|
+
"`# rigor:disable <rules>` (suppresses on its own line) and " \
|
|
595
|
+
"`# rigor:disable-file <rules>`, so this comment suppresses nothing.",
|
|
596
|
+
severity: :warning,
|
|
597
|
+
rule: RULE_SUPPRESSION_UNKNOWN_MARKER,
|
|
598
|
+
source_family: :builtin
|
|
599
|
+
)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def empty_suppression_diagnostic(path, comment, marker)
|
|
603
|
+
Diagnostic.new(
|
|
604
|
+
path: path,
|
|
605
|
+
line: comment.location.start_line,
|
|
606
|
+
column: comment.location.start_column + 1,
|
|
607
|
+
message: "`# #{marker}` lists no rules, so this suppression has no effect. Name the rules to " \
|
|
608
|
+
"suppress (`# #{marker} call.undefined-method`) or use `# #{marker} all`.",
|
|
609
|
+
severity: :warning,
|
|
610
|
+
rule: RULE_SUPPRESSION_EMPTY,
|
|
611
|
+
source_family: :builtin
|
|
612
|
+
)
|
|
613
|
+
end
|
|
614
|
+
|
|
481
615
|
# Expands a list of user-supplied rule tokens into the
|
|
482
616
|
# canonical-id set per ADR-8 § "Backward compatibility".
|
|
483
617
|
# `disabled_rules` accepts unprefixed legacy names
|
|
@@ -500,12 +634,16 @@ module Rigor
|
|
|
500
634
|
class << self
|
|
501
635
|
private
|
|
502
636
|
|
|
503
|
-
def undefined_method_diagnostic(path, call_node, scope_index)
|
|
637
|
+
def undefined_method_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
504
638
|
return nil if call_node.receiver.nil?
|
|
505
639
|
|
|
506
640
|
scope = scope_index[call_node]
|
|
507
641
|
return nil if scope.nil?
|
|
508
642
|
|
|
643
|
+
# ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound; firing
|
|
644
|
+
# undefined-method against it is an FP by construction. Decline.
|
|
645
|
+
return nil if inferred_param_receiver?(call_node, scope)
|
|
646
|
+
|
|
509
647
|
# N3 — a safe-navigation call (`recv&.m`) never dispatches on the
|
|
510
648
|
# nil edge of its receiver: at runtime it short-circuits to nil.
|
|
511
649
|
# A receiver that types as exactly `nil` yields nil with no call at
|
|
@@ -727,6 +865,28 @@ module Rigor
|
|
|
727
865
|
}.freeze
|
|
728
866
|
private_constant :CONSTANT_CLASSES
|
|
729
867
|
|
|
868
|
+
# ADR-67 WD6b — true when this call's receiver is *rooted at* a pristine inferred-parameter local (the
|
|
869
|
+
# call-site union seeded at method entry). Every negative in-body rule (`call.undefined-method`,
|
|
870
|
+
# wrong-arity, argument-type-mismatch, possible-nil, visibility) declines on such a receiver: an open
|
|
871
|
+
# call-site set makes the inferred type a *lower bound*, so a diagnostic against it is a false positive
|
|
872
|
+
# by construction (the same reasoning that keeps ADR-67 WD1 non-negotiable at the parameter boundary,
|
|
873
|
+
# carried into the body). No-op on a normal `check` run — `inferred_param?` is false unless the
|
|
874
|
+
# `parameter_inference:` gate seeded the table.
|
|
875
|
+
def inferred_param_receiver?(call_node, scope)
|
|
876
|
+
inferred_param_rooted?(call_node.receiver, scope)
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
# ADR-67 WD6b — the argument-position analogue: true when `arg` is rooted at an inferred-parameter
|
|
880
|
+
# local. The argument-type-mismatch rule declines on it for the same lower-bound reason.
|
|
881
|
+
def inferred_param_argument?(arg, scope)
|
|
882
|
+
inferred_param_rooted?(arg, scope)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# ADR-67 WD6b — see {CheckRules::InferredParamGuard} for the shared root-walk contract.
|
|
886
|
+
def inferred_param_rooted?(node, scope)
|
|
887
|
+
InferredParamGuard.rooted?(node, scope)
|
|
888
|
+
end
|
|
889
|
+
|
|
730
890
|
def constant_class_name(value)
|
|
731
891
|
CONSTANT_CLASSES.each { |klass, name| return name if value.is_a?(klass) }
|
|
732
892
|
nil
|
|
@@ -942,6 +1102,10 @@ module Rigor
|
|
|
942
1102
|
scope = scope_index[call_node]
|
|
943
1103
|
return nil if scope.nil?
|
|
944
1104
|
|
|
1105
|
+
# ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound; a wrong-arity
|
|
1106
|
+
# firing against it is an FP by construction. Decline.
|
|
1107
|
+
return nil if inferred_param_receiver?(call_node, scope)
|
|
1108
|
+
|
|
945
1109
|
receiver_type = scope.type_of(call_node.receiver)
|
|
946
1110
|
class_name = concrete_class_name(receiver_type)
|
|
947
1111
|
return nil if class_name.nil?
|
|
@@ -1094,6 +1258,11 @@ module Rigor
|
|
|
1094
1258
|
# flow-observed nil keeps firing exactly as before.
|
|
1095
1259
|
return nil if scope.declaration_sourced?(:local, call_node.receiver.name)
|
|
1096
1260
|
|
|
1261
|
+
# ADR-67 WD6b — an inferred-parameter receiver's type (incl. any nil constituent unioned in from a
|
|
1262
|
+
# nil call site) is an open-call-site lower bound; a possible-nil firing against it is an FP by
|
|
1263
|
+
# construction. Decline.
|
|
1264
|
+
return nil if scope.inferred_param?(call_node.receiver.name)
|
|
1265
|
+
|
|
1097
1266
|
receiver_type = scope.type_of(call_node.receiver)
|
|
1098
1267
|
return nil unless receiver_type.is_a?(Type::Union)
|
|
1099
1268
|
|
|
@@ -1396,6 +1565,178 @@ module Rigor
|
|
|
1396
1565
|
)
|
|
1397
1566
|
end
|
|
1398
1567
|
|
|
1568
|
+
# `call.raise-non-exception` — `raise x` / `fail x` where the first argument's statically-inferred
|
|
1569
|
+
# type is provably NOT a legal raise operand (PHPStan ThrowExprTypeRule analogue). Legal operands:
|
|
1570
|
+
# an Exception class object, an Exception instance, a String (raises RuntimeError), or any object
|
|
1571
|
+
# whose class defines `#exception` (the duck protocol `raise` consults at runtime). Anything else
|
|
1572
|
+
# (`raise 42`, `raise :sym`, `raise nil` — an explicit nil argument is a TypeError, unlike bare
|
|
1573
|
+
# `raise` which re-raises `$!`) raises TypeError at runtime.
|
|
1574
|
+
#
|
|
1575
|
+
# Conservative envelope (FP discipline):
|
|
1576
|
+
# - Implicit-self `raise` / `fail` only; an explicit-receiver call is a user method, not Kernel#raise.
|
|
1577
|
+
# - Silent when the project redefines `raise` / `fail` anywhere the call could resolve (a same-file
|
|
1578
|
+
# toplevel def, an Object/Kernel monkey-patch, or a def on the enclosing class — in source or via
|
|
1579
|
+
# `pre_eval:`).
|
|
1580
|
+
# - Only the first positional argument is checked; splat / kwargs / forwarded first args bail.
|
|
1581
|
+
# - Fires only on a concrete verdict: Dynamic / Top / unknown / unresolved types are silent, and a
|
|
1582
|
+
# Union fires only when EVERY arm is independently illegal.
|
|
1583
|
+
# - A Class operand (`Type::Singleton`) is exact, so both `:disjoint` and `:superclass` orderings
|
|
1584
|
+
# against Exception are provably illegal — but only when the class is RBS-known, not an ADR-26
|
|
1585
|
+
# open receiver / synthesized stub, and its singleton defines no `exception` method.
|
|
1586
|
+
# - An instance operand's nominal class is NOT exact (a runtime subclass could define `#exception`
|
|
1587
|
+
# or be an Exception), so only `:disjoint` fires, and the generic carriers (`Class` / `Module` /
|
|
1588
|
+
# `Object` / `BasicObject`) plus module-typed values (any includer could be an Exception) bail.
|
|
1589
|
+
RAISE_METHOD_NAMES = %i[raise fail].freeze
|
|
1590
|
+
private_constant :RAISE_METHOD_NAMES
|
|
1591
|
+
|
|
1592
|
+
# Instance types whose nominal class subsumes exception values (or class objects), so a "disjoint
|
|
1593
|
+
# from Exception" ordering proves nothing about the runtime value. `Object` / `BasicObject` order as
|
|
1594
|
+
# `:superclass` (already silent) but are listed for explicitness.
|
|
1595
|
+
RAISE_UNEXACT_INSTANCE_CLASSES = %w[Class Module Object BasicObject].freeze
|
|
1596
|
+
private_constant :RAISE_UNEXACT_INSTANCE_CLASSES
|
|
1597
|
+
|
|
1598
|
+
def raise_non_exception_diagnostic(path, call_node, scope_index)
|
|
1599
|
+
return nil unless call_node.receiver.nil?
|
|
1600
|
+
return nil unless RAISE_METHOD_NAMES.include?(call_node.name)
|
|
1601
|
+
return nil unless call_node.block.nil?
|
|
1602
|
+
|
|
1603
|
+
arg = first_positional_raise_operand(call_node)
|
|
1604
|
+
return nil if arg.nil?
|
|
1605
|
+
|
|
1606
|
+
scope = scope_index[arg] || scope_index[call_node]
|
|
1607
|
+
return nil if scope.nil?
|
|
1608
|
+
return nil if raise_redefined_in_scope?(scope, call_node.name)
|
|
1609
|
+
|
|
1610
|
+
operand_type = scope.type_of(arg)
|
|
1611
|
+
return nil unless raise_operand_verdict(operand_type, scope) == :illegal
|
|
1612
|
+
|
|
1613
|
+
build_raise_non_exception_diagnostic(path, call_node, operand_type)
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
def first_positional_raise_operand(call_node)
|
|
1617
|
+
args = call_node.arguments&.arguments
|
|
1618
|
+
return nil if args.nil? || args.empty?
|
|
1619
|
+
|
|
1620
|
+
first = args.first
|
|
1621
|
+
return nil if first.is_a?(Prism::SplatNode) || first.is_a?(Prism::KeywordHashNode) ||
|
|
1622
|
+
first.is_a?(Prism::BlockArgumentNode) || first.is_a?(Prism::ForwardingArgumentsNode)
|
|
1623
|
+
|
|
1624
|
+
first
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
# True when a project-side definition of `raise` / `fail` could shadow Kernel's at this call site:
|
|
1628
|
+
# a same-file toplevel def, a monkey-patch on Object / Kernel (in source or `pre_eval:`), or a def
|
|
1629
|
+
# on the enclosing class (instance or singleton side — implicit self dispatches to either depending
|
|
1630
|
+
# on context, and being silent for both is the cheap conservative answer).
|
|
1631
|
+
def raise_redefined_in_scope?(scope, name)
|
|
1632
|
+
return true if scope.top_level_def_for(name)
|
|
1633
|
+
return true if source_declared_method?(scope, "Object", name, :instance)
|
|
1634
|
+
return true if source_declared_method?(scope, "Kernel", name, :instance)
|
|
1635
|
+
|
|
1636
|
+
self_type = scope.self_type
|
|
1637
|
+
return false unless self_type.respond_to?(:class_name)
|
|
1638
|
+
|
|
1639
|
+
class_name = self_type.class_name
|
|
1640
|
+
return false if class_name.nil?
|
|
1641
|
+
|
|
1642
|
+
source_declared_method?(scope, class_name, name, :instance) ||
|
|
1643
|
+
source_declared_method?(scope, class_name, name, :singleton)
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
# Trinary verdict — `:legal` / `:illegal` / `:unknown`. Only `:illegal` fires; anything the engine
|
|
1647
|
+
# cannot prove stays `:unknown` (silent).
|
|
1648
|
+
def raise_operand_verdict(type, scope)
|
|
1649
|
+
case type
|
|
1650
|
+
when Type::Union
|
|
1651
|
+
verdicts = type.members.map { |member| raise_operand_verdict(member, scope) }
|
|
1652
|
+
return :illegal if verdicts.all?(:illegal)
|
|
1653
|
+
|
|
1654
|
+
verdicts.all?(:legal) ? :legal : :unknown
|
|
1655
|
+
when Type::Singleton
|
|
1656
|
+
raise_class_operand_verdict(type.class_name, scope)
|
|
1657
|
+
else
|
|
1658
|
+
raise_instance_operand_verdict(type, scope)
|
|
1659
|
+
end
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
# A `Type::Singleton` names ONE exact class / module object, so an ordering that places it outside
|
|
1663
|
+
# Exception's ancestry (`:disjoint`, or `:superclass` — e.g. `raise Object`) is a proof, provided
|
|
1664
|
+
# the singleton also lacks an `exception` method (`raise Klass` calls `Klass.exception`). A module
|
|
1665
|
+
# constant orders `:disjoint` and fires unless its singleton defines `exception`.
|
|
1666
|
+
def raise_class_operand_verdict(class_name, scope)
|
|
1667
|
+
return :unknown if class_name.nil? || scope.environment.nil?
|
|
1668
|
+
return :unknown if unbounded_receiver_surface?(class_name, scope)
|
|
1669
|
+
return :unknown if Rigor::Reflection.discovered_class?(class_name, scope: scope)
|
|
1670
|
+
return :unknown unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
1671
|
+
|
|
1672
|
+
case Rigor::Reflection.class_ordering(class_name, "Exception", scope: scope)
|
|
1673
|
+
when :equal, :subclass then :legal
|
|
1674
|
+
when :superclass, :disjoint
|
|
1675
|
+
raise_duck_exception?(class_name, :singleton, scope) ? :legal : :illegal
|
|
1676
|
+
else
|
|
1677
|
+
:unknown
|
|
1678
|
+
end
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
# An instance operand: legal when its class is String-family (raises RuntimeError) or an Exception
|
|
1682
|
+
# descendant; illegal only when the class is fully known, exact enough (not a generic metaclass /
|
|
1683
|
+
# module carrier), provably disjoint from BOTH, and defines no `#exception`. `:superclass` stays
|
|
1684
|
+
# silent — a value typed `Object` may well BE an Exception at runtime.
|
|
1685
|
+
def raise_instance_operand_verdict(type, scope)
|
|
1686
|
+
class_name = concrete_class_name(type)
|
|
1687
|
+
return :unknown if class_name.nil? || scope.environment.nil?
|
|
1688
|
+
return :unknown if RAISE_UNEXACT_INSTANCE_CLASSES.include?(class_name)
|
|
1689
|
+
return :unknown if unbounded_receiver_surface?(class_name, scope)
|
|
1690
|
+
# A project-declared class's ancestry must not be proven from RBS alone: a `sig/` declaration that
|
|
1691
|
+
# omits the superclass (`class Conflict` for a source-side `class Conflict < StandardError`)
|
|
1692
|
+
# defaults to Object in the RBS env, which would read as "disjoint from Exception" for a class
|
|
1693
|
+
# that IS one at runtime (the rule's first self-check firing, `Plugin::FactStore::Conflict`).
|
|
1694
|
+
# Source-discovered classes stay silent.
|
|
1695
|
+
return :unknown if Rigor::Reflection.discovered_class?(class_name, scope: scope)
|
|
1696
|
+
return :unknown unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
1697
|
+
return :unknown if scope.environment.rbs_module?(class_name)
|
|
1698
|
+
|
|
1699
|
+
string_ordering = Rigor::Reflection.class_ordering(class_name, "String", scope: scope)
|
|
1700
|
+
return :legal if %i[equal subclass].include?(string_ordering)
|
|
1701
|
+
|
|
1702
|
+
case Rigor::Reflection.class_ordering(class_name, "Exception", scope: scope)
|
|
1703
|
+
when :equal, :subclass then :legal
|
|
1704
|
+
when :disjoint
|
|
1705
|
+
raise_duck_exception?(class_name, :instance, scope) ? :legal : :illegal
|
|
1706
|
+
else
|
|
1707
|
+
:unknown
|
|
1708
|
+
end
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
# Whether the class carries an `exception` method on the given side — from RBS, an in-source `def`,
|
|
1712
|
+
# or a `pre_eval:` patch. An unbuildable definition returns true (assume the duck) so structural
|
|
1713
|
+
# RBS gaps never manufacture a firing.
|
|
1714
|
+
def raise_duck_exception?(class_name, kind, scope)
|
|
1715
|
+
return true if source_declared_method?(scope, class_name, :exception, kind)
|
|
1716
|
+
|
|
1717
|
+
definition = if kind == :singleton
|
|
1718
|
+
Rigor::Reflection.singleton_definition(class_name, scope: scope)
|
|
1719
|
+
else
|
|
1720
|
+
Rigor::Reflection.instance_definition(class_name, scope: scope)
|
|
1721
|
+
end
|
|
1722
|
+
return true if definition.nil?
|
|
1723
|
+
|
|
1724
|
+
!definition.methods[:exception].nil?
|
|
1725
|
+
end
|
|
1726
|
+
|
|
1727
|
+
def build_raise_non_exception_diagnostic(path, call_node, operand_type)
|
|
1728
|
+
Diagnostic.from_message_loc(
|
|
1729
|
+
call_node,
|
|
1730
|
+
rule: RULE_RAISE_NON_EXCEPTION,
|
|
1731
|
+
path: path,
|
|
1732
|
+
message: "`#{call_node.name}' operand types as #{operand_type.describe(:short)}, which is not " \
|
|
1733
|
+
"an Exception class, an Exception instance, a String, or an object defining " \
|
|
1734
|
+
"`#exception' — this raises TypeError at runtime",
|
|
1735
|
+
severity: :error,
|
|
1736
|
+
method_name: call_node.name.to_s
|
|
1737
|
+
)
|
|
1738
|
+
end
|
|
1739
|
+
|
|
1399
1740
|
# v0.1.2 — `flow.unreachable-branch`. Fires when an `IfNode` / `UnlessNode` whose predicate is a
|
|
1400
1741
|
# literal `true` / `false` / `nil` (or a literal numeric / string / symbol whose Ruby truthiness is
|
|
1401
1742
|
# known at-a-glance) has an observable dead branch. The diagnostic points at the dead branch (not
|
|
@@ -1468,6 +1809,10 @@ module Rigor
|
|
|
1468
1809
|
scope = scope_index[call_node]
|
|
1469
1810
|
return nil if scope.nil?
|
|
1470
1811
|
|
|
1812
|
+
# ADR-67 WD6b — an inferred-parameter receiver's nominal type is an open-call-site lower bound; a
|
|
1813
|
+
# private-method visibility firing against it is an FP by construction. Decline.
|
|
1814
|
+
return nil if inferred_param_receiver?(call_node, scope)
|
|
1815
|
+
|
|
1471
1816
|
receiver_type = scope.type_of(call_node.receiver)
|
|
1472
1817
|
return nil unless receiver_type.is_a?(Type::Nominal)
|
|
1473
1818
|
|
|
@@ -1532,6 +1877,17 @@ module Rigor
|
|
|
1532
1877
|
)
|
|
1533
1878
|
end
|
|
1534
1879
|
|
|
1880
|
+
def build_void_value_use_diagnostic(path, result)
|
|
1881
|
+
Diagnostic.from_node(
|
|
1882
|
+
result.void_node,
|
|
1883
|
+
rule: RULE_VALUE_USE_VOID,
|
|
1884
|
+
path: path,
|
|
1885
|
+
message: "value use of `void': `#{result.origin.label}' declares `-> void', so its return " \
|
|
1886
|
+
"recovers to `top' and should not be used as a value",
|
|
1887
|
+
severity: :warning
|
|
1888
|
+
)
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1535
1891
|
def unreachable_clause_message(result)
|
|
1536
1892
|
subject = result.subject_name
|
|
1537
1893
|
kw = result.keyword
|
|
@@ -1547,6 +1903,24 @@ module Rigor
|
|
|
1547
1903
|
end
|
|
1548
1904
|
end
|
|
1549
1905
|
|
|
1906
|
+
def build_shadowed_rescue_diagnostic(path, result)
|
|
1907
|
+
Diagnostic.from_node(
|
|
1908
|
+
result.clause,
|
|
1909
|
+
rule: RULE_SHADOWED_RESCUE_CLAUSE,
|
|
1910
|
+
path: path,
|
|
1911
|
+
message: shadowed_rescue_message(result),
|
|
1912
|
+
severity: :warning
|
|
1913
|
+
)
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
def shadowed_rescue_message(result)
|
|
1917
|
+
earlier = result.earlier_sources.each_with_index.map do |source, index|
|
|
1918
|
+
"`#{source}' (line #{result.earlier_lines[index]})"
|
|
1919
|
+
end
|
|
1920
|
+
"shadowed `#{result.clause_source}': every exception class it names is already caught " \
|
|
1921
|
+
"by the earlier #{earlier.join(' and ')} clause#{'s' if earlier.size > 1}, so this clause can never run"
|
|
1922
|
+
end
|
|
1923
|
+
|
|
1550
1924
|
def build_dead_assignment_diagnostic(path, write_node, def_node)
|
|
1551
1925
|
Diagnostic.from_name_loc(
|
|
1552
1926
|
write_node,
|
|
@@ -1557,6 +1931,32 @@ module Rigor
|
|
|
1557
1931
|
)
|
|
1558
1932
|
end
|
|
1559
1933
|
|
|
1934
|
+
# The diagnostic points at the LATER occurrence (the entry that wins at runtime) and names the
|
|
1935
|
+
# first occurrence's line, so the fix — delete or rename one of the two — is visible from the
|
|
1936
|
+
# message alone.
|
|
1937
|
+
def build_duplicate_hash_key_diagnostic(path, result)
|
|
1938
|
+
first_line = result[:first_key_node].location.start_line
|
|
1939
|
+
Diagnostic.from_node(
|
|
1940
|
+
result[:key_node],
|
|
1941
|
+
rule: RULE_DUPLICATE_HASH_KEY,
|
|
1942
|
+
path: path,
|
|
1943
|
+
message: "duplicate hash key `#{result[:key_label]}' in the same literal; this entry " \
|
|
1944
|
+
"overwrites the value first set at line #{first_line}",
|
|
1945
|
+
severity: :warning
|
|
1946
|
+
)
|
|
1947
|
+
end
|
|
1948
|
+
|
|
1949
|
+
def build_return_in_ensure_diagnostic(path, return_node)
|
|
1950
|
+
Diagnostic.from_location(
|
|
1951
|
+
return_node.keyword_loc,
|
|
1952
|
+
rule: RULE_RETURN_IN_ENSURE,
|
|
1953
|
+
path: path,
|
|
1954
|
+
message: "`return' inside `ensure' discards the method's in-flight return value " \
|
|
1955
|
+
"and swallows any in-flight exception",
|
|
1956
|
+
severity: :warning
|
|
1957
|
+
)
|
|
1958
|
+
end
|
|
1959
|
+
|
|
1560
1960
|
def build_ivar_write_mismatch_diagnostic(path, node, class_name, ivar_name, first_class, other_class)
|
|
1561
1961
|
Diagnostic.from_name_loc(
|
|
1562
1962
|
node,
|
|
@@ -1675,6 +2075,9 @@ module Rigor
|
|
|
1675
2075
|
param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
|
|
1676
2076
|
mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides)
|
|
1677
2077
|
return nil if mismatch.nil?
|
|
2078
|
+
# ADR-67 WD6b — the mismatching argument is an inferred-parameter local, whose type is an
|
|
2079
|
+
# open-call-site lower bound; firing argument-type-mismatch against it is an FP by construction.
|
|
2080
|
+
return nil if inferred_param_argument?(mismatch[:node], scope)
|
|
1678
2081
|
|
|
1679
2082
|
build_argument_type_diagnostic(path, call_node, class_name, mismatch)
|
|
1680
2083
|
end
|
|
@@ -1716,7 +2119,7 @@ module Rigor
|
|
|
1716
2119
|
arguments = call_node.arguments&.arguments || []
|
|
1717
2120
|
arguments.each_with_index do |arg, index|
|
|
1718
2121
|
arg_type = scope.type_of(arg)
|
|
1719
|
-
params = overload_positional_params(
|
|
2122
|
+
params = overload_positional_params(method_types, index)
|
|
1720
2123
|
next if params.nil? # arity divergence — some overload lacks a param here
|
|
1721
2124
|
|
|
1722
2125
|
mismatch =
|
|
@@ -1751,11 +2154,30 @@ module Rigor
|
|
|
1751
2154
|
# The matching positional RBS param across every overload, or nil when
|
|
1752
2155
|
# any overload has no param at `index` (arity divergence — the
|
|
1753
2156
|
# wrong-arity rule's concern, not this one's).
|
|
1754
|
-
def overload_positional_params(
|
|
1755
|
-
params =
|
|
2157
|
+
def overload_positional_params(method_types, index)
|
|
2158
|
+
params = method_types.map do |method_type|
|
|
2159
|
+
function = method_type.type
|
|
2160
|
+
param = (function.required_positionals + function.optional_positionals)[index]
|
|
2161
|
+
param && resolve_param_bounds(param, method_type)
|
|
2162
|
+
end
|
|
1756
2163
|
params.any?(&:nil?) ? nil : params
|
|
1757
2164
|
end
|
|
1758
2165
|
|
|
2166
|
+
# Substitutes each bounded method-level type parameter for its bound, so
|
|
2167
|
+
# `[I < _ToInt] (I index) -> …` is walked as `(_ToInt index) -> …`. A bare
|
|
2168
|
+
# `Variable` is undecidable to the acceptance walk and admits everything,
|
|
2169
|
+
# which silently disables both channels for the whole overload; the bound
|
|
2170
|
+
# constrains the argument exactly as an ordinary param of that type would.
|
|
2171
|
+
# Load-bearing since rbs 4.1 rewrote core signatures into this form
|
|
2172
|
+
# (`Array#fetch`'s block overload is `[I < _ToInt, T] (I index) { … }`).
|
|
2173
|
+
def resolve_param_bounds(param, method_type)
|
|
2174
|
+
bounded = method_type.type_params.select(&:upper_bound)
|
|
2175
|
+
return param if bounded.empty?
|
|
2176
|
+
|
|
2177
|
+
substitution = RBS::Substitution.build(bounded.map(&:name), bounded.map(&:upper_bound))
|
|
2178
|
+
param.map_type { |type| type.sub(substitution) }
|
|
2179
|
+
end
|
|
2180
|
+
|
|
1759
2181
|
# The class names whose instances `nil` IS — `NilClass` and every
|
|
1760
2182
|
# ancestor. A parameter typed as any other class instance rejects nil.
|
|
1761
2183
|
NIL_COMPATIBLE_CLASS_NAMES = %w[NilClass Object BasicObject Kernel].to_set.freeze
|
|
@@ -2169,11 +2591,16 @@ module Rigor
|
|
|
2169
2591
|
declared_return_union(method_def, scope.environment)
|
|
2170
2592
|
end
|
|
2171
2593
|
|
|
2172
|
-
|
|
2594
|
+
# `type_vars:` — ADR-35 WD9 tier 1. When the caller supplies a generic-instantiation
|
|
2595
|
+
# substitution map (parent-side, keyed by the parent class's declared type-parameter names),
|
|
2596
|
+
# a `-> T` return is translated at its instantiated type (`-> Integer`) rather than degrading
|
|
2597
|
+
# to `Dynamic[Top]`. The default empty map is the pre-WD9 behaviour, under which any type
|
|
2598
|
+
# variable degrades to `Dynamic[Top]` and the rule stays silent.
|
|
2599
|
+
def declared_return_union(method_def, _environment, type_vars: {})
|
|
2173
2600
|
translated = method_def.method_types.filter_map do |mt|
|
|
2174
2601
|
Inference::RbsTypeTranslator.translate(
|
|
2175
2602
|
mt.type.return_type,
|
|
2176
|
-
self_type: nil, instance_type: nil, type_vars:
|
|
2603
|
+
self_type: nil, instance_type: nil, type_vars: type_vars
|
|
2177
2604
|
)
|
|
2178
2605
|
rescue StandardError
|
|
2179
2606
|
nil
|
|
@@ -2401,8 +2828,9 @@ module Rigor
|
|
|
2401
2828
|
return nil if resolved.nil?
|
|
2402
2829
|
|
|
2403
2830
|
scope, override_method, parent_class, parent_method = resolved
|
|
2831
|
+
parent_type_vars = ancestor_instantiation_type_vars(scope, parent_class)
|
|
2404
2832
|
override_return = declared_return_union(override_method, scope.environment)
|
|
2405
|
-
parent_return = declared_return_union(parent_method, scope.environment)
|
|
2833
|
+
parent_return = declared_return_union(parent_method, scope.environment, type_vars: parent_type_vars)
|
|
2406
2834
|
return nil if override_return.nil? || parent_return.nil?
|
|
2407
2835
|
return nil if dynamic_top?(parent_return) # untyped / unbound-generic parent contract
|
|
2408
2836
|
|
|
@@ -2441,6 +2869,56 @@ module Rigor
|
|
|
2441
2869
|
name.to_s.delete_prefix("::")
|
|
2442
2870
|
end
|
|
2443
2871
|
|
|
2872
|
+
# ADR-35 WD9 tier 1 — generic-instantiation-aware comparison. Builds the substitution map
|
|
2873
|
+
# `{ parent_type_param_name => instantiated Rigor::Type }` for the parent contract as the
|
|
2874
|
+
# overriding subclass instantiates it (RBS `class Sub < Parent[Concrete]` / `include
|
|
2875
|
+
# _Iface[Concrete]`). Mirrors the ADR-4 Phase 2d dispatcher zip (`build_type_vars`): the
|
|
2876
|
+
# parent's declared type-parameter names zipped against the subclass-side instantiation args.
|
|
2877
|
+
#
|
|
2878
|
+
# Returns `{}` — restoring the pre-WD9 behaviour under which any parent type variable degrades
|
|
2879
|
+
# to `Dynamic[Top]` and the rule stays silent — whenever the subclass does not instantiate the
|
|
2880
|
+
# ancestor generically, the RBS definition cannot be built, arities disagree, or an argument
|
|
2881
|
+
# fails to translate. This is the FP-safety contract: substitution only ever *adds* precision
|
|
2882
|
+
# (a `-> T` compared at `-> Integer`); an unbound / unresolved generic keeps degrading to
|
|
2883
|
+
# silence, never a false `:no`.
|
|
2884
|
+
def ancestor_instantiation_type_vars(scope, parent_class)
|
|
2885
|
+
self_type = scope.self_type
|
|
2886
|
+
return {} unless self_type.respond_to?(:class_name)
|
|
2887
|
+
|
|
2888
|
+
args = ancestor_instantiation_args(scope, self_type.class_name.to_s, parent_class)
|
|
2889
|
+
return {} if args.nil? || args.empty?
|
|
2890
|
+
|
|
2891
|
+
param_names = Reflection.class_type_param_names(parent_class, scope: scope)
|
|
2892
|
+
return {} if param_names.empty? || param_names.size != args.size
|
|
2893
|
+
|
|
2894
|
+
translated = args.map do |arg|
|
|
2895
|
+
Inference::RbsTypeTranslator.translate(arg, self_type: nil, instance_type: nil, type_vars: {})
|
|
2896
|
+
rescue StandardError
|
|
2897
|
+
nil
|
|
2898
|
+
end
|
|
2899
|
+
return {} if translated.any?(&:nil?)
|
|
2900
|
+
|
|
2901
|
+
param_names.zip(translated).to_h
|
|
2902
|
+
end
|
|
2903
|
+
|
|
2904
|
+
# The RBS type arguments the subclass applies to `parent_class` in its ancestry, or nil when
|
|
2905
|
+
# the subclass has no RBS definition or does not name that ancestor. Reads the resolved
|
|
2906
|
+
# instance-ancestor list (`RBS::Definition#ancestors`), whose `Ancestor::Instance` entries carry
|
|
2907
|
+
# the instantiation `.args` for each superclass / included module. Fail-soft: any RBS build
|
|
2908
|
+
# error yields nil, which the caller treats as "no instantiation" and degrades to silence.
|
|
2909
|
+
def ancestor_instantiation_args(scope, subclass_name, parent_class)
|
|
2910
|
+
definition = Reflection.instance_definition(subclass_name, scope: scope)
|
|
2911
|
+
return nil if definition.nil?
|
|
2912
|
+
|
|
2913
|
+
target = normalize_class_name(parent_class)
|
|
2914
|
+
ancestor = definition.ancestors.ancestors.find do |anc|
|
|
2915
|
+
anc.respond_to?(:args) && normalize_class_name(anc.name.to_s) == target
|
|
2916
|
+
end
|
|
2917
|
+
ancestor&.args
|
|
2918
|
+
rescue ::RBS::BaseError, StandardError
|
|
2919
|
+
nil
|
|
2920
|
+
end
|
|
2921
|
+
|
|
2444
2922
|
def build_override_return_widened_diagnostic(path, def_node, parent_class, parent_return, override_return)
|
|
2445
2923
|
Diagnostic.from_name_loc(
|
|
2446
2924
|
def_node,
|
|
@@ -2469,9 +2947,10 @@ module Rigor
|
|
|
2469
2947
|
resolved = resolve_authored_override(def_node, scope_index)
|
|
2470
2948
|
return nil if resolved.nil?
|
|
2471
2949
|
|
|
2472
|
-
|
|
2950
|
+
scope, override_method, parent_class, parent_method = resolved
|
|
2951
|
+
parent_type_vars = ancestor_instantiation_type_vars(scope, parent_class)
|
|
2473
2952
|
override_params = positional_param_types(override_method)
|
|
2474
|
-
parent_params = positional_param_types(parent_method)
|
|
2953
|
+
parent_params = positional_param_types(parent_method, type_vars: parent_type_vars)
|
|
2475
2954
|
return nil if override_params.nil? || parent_params.nil?
|
|
2476
2955
|
|
|
2477
2956
|
index = first_narrowed_param_index(override_params, parent_params)
|
|
@@ -2487,7 +2966,11 @@ module Rigor
|
|
|
2487
2966
|
# parameter list is not introspectable. Per-position translation failures yield `nil` at that slot
|
|
2488
2967
|
# (skipped by the comparison). `self`/`instance` translate with `self_type: nil` (→
|
|
2489
2968
|
# `Dynamic[Top]`), matching the return-side handling.
|
|
2490
|
-
|
|
2969
|
+
# `type_vars:` — ADR-35 WD9 tier 1, parent side only. Substitutes the generic-instantiation
|
|
2970
|
+
# map (parent class's type params → the subclass's instantiation) so a parent `(T)` parameter
|
|
2971
|
+
# is compared at its instantiated type. Empty (the default, and the override side) keeps the
|
|
2972
|
+
# pre-WD9 degrade-to-`Dynamic[Top]` behaviour.
|
|
2973
|
+
def positional_param_types(method_def, type_vars: {})
|
|
2491
2974
|
method_types = method_def.method_types
|
|
2492
2975
|
return nil unless method_types.size == 1
|
|
2493
2976
|
|
|
@@ -2496,7 +2979,7 @@ module Rigor
|
|
|
2496
2979
|
|
|
2497
2980
|
(func.required_positionals + func.optional_positionals).map do |param|
|
|
2498
2981
|
Inference::RbsTypeTranslator.translate(
|
|
2499
|
-
param.type, self_type: nil, instance_type: nil, type_vars:
|
|
2982
|
+
param.type, self_type: nil, instance_type: nil, type_vars: type_vars
|
|
2500
2983
|
)
|
|
2501
2984
|
rescue StandardError
|
|
2502
2985
|
nil
|