rigortype 0.1.10 → 0.1.12
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/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli.rb +44 -3
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +181 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +199 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +398 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +86 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +183 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +713 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +201 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +226 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +261 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +283 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +250 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +98 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +590 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +37 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +478 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +353 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +175 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +350 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +164 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1538 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +235 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/scope.rbs +22 -0
- metadata +157 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renderer for `rigor plugins`. Produces a human-readable
|
|
8
|
+
# text table and a JSON representation from the same row
|
|
9
|
+
# shape (the Hash documented on
|
|
10
|
+
# {Rigor::CLI::PluginsCommand#loaded_row}).
|
|
11
|
+
#
|
|
12
|
+
# The two formats carry the same content; JSON is meant for
|
|
13
|
+
# tooling (SKILLs, CI, editor integrations) while text is
|
|
14
|
+
# for interactive inspection. Rows are printed in the order
|
|
15
|
+
# the loader resolved them.
|
|
16
|
+
class PluginsRenderer
|
|
17
|
+
def initialize(rows:, configuration_path:)
|
|
18
|
+
@rows = rows
|
|
19
|
+
@configuration_path = configuration_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def text
|
|
23
|
+
lines = []
|
|
24
|
+
lines << header
|
|
25
|
+
lines << ""
|
|
26
|
+
@rows.each_with_index do |row, index|
|
|
27
|
+
lines.concat(row_lines(row))
|
|
28
|
+
lines << "" unless index == @rows.size - 1
|
|
29
|
+
end
|
|
30
|
+
lines << ""
|
|
31
|
+
lines << footer
|
|
32
|
+
lines.join("\n")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def json
|
|
36
|
+
JSON.pretty_generate(
|
|
37
|
+
{
|
|
38
|
+
"configuration" => @configuration_path,
|
|
39
|
+
"plugins" => @rows.map { |row| row_json(row) },
|
|
40
|
+
"summary" => summary
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def header
|
|
48
|
+
loaded = @rows.count { |r| r[:status] == :loaded }
|
|
49
|
+
errored = @rows.count { |r| r[:status] == :load_error }
|
|
50
|
+
config = @configuration_path || "(no .rigor.yml found; using defaults)"
|
|
51
|
+
"Plugin activation report\n " \
|
|
52
|
+
"configuration: #{config}\n " \
|
|
53
|
+
"loaded: #{loaded} load-error: #{errored}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def footer
|
|
57
|
+
errored = @rows.select { |r| r[:status] == :load_error }
|
|
58
|
+
if errored.empty?
|
|
59
|
+
"All configured plugins loaded successfully."
|
|
60
|
+
else
|
|
61
|
+
"#{errored.size} plugin(s) failed to load — see above. " \
|
|
62
|
+
"Run with --strict to make this a CI gate."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def row_lines(row)
|
|
67
|
+
marker = row[:status] == :loaded ? "OK " : "ERR"
|
|
68
|
+
head = if row[:status] == :loaded
|
|
69
|
+
" [#{marker}] #{row[:id]} v#{row[:version]} (#{row[:gem]})"
|
|
70
|
+
else
|
|
71
|
+
" [#{marker}] #{row[:gem]}"
|
|
72
|
+
end
|
|
73
|
+
lines = [head]
|
|
74
|
+
lines << " #{row[:description]}" if row[:description]
|
|
75
|
+
|
|
76
|
+
if row[:status] == :load_error
|
|
77
|
+
lines << " load error: #{row[:load_error]}"
|
|
78
|
+
lines << " config: #{row[:config].inspect}" if row[:config] && !row[:config].empty?
|
|
79
|
+
return lines
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
lines.concat(loaded_detail_lines(row))
|
|
83
|
+
lines
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def loaded_detail_lines(row)
|
|
87
|
+
lines = []
|
|
88
|
+
lines.concat(signature_paths_lines(row[:signature_paths])) if row[:signature_paths].any?
|
|
89
|
+
lines.concat(receiver_and_fact_lines(row))
|
|
90
|
+
lines.concat(macro_substrate_lines(row))
|
|
91
|
+
lines.concat(extra_surface_lines(row))
|
|
92
|
+
lines << " config: #{row[:config].inspect}" if row[:config] && !row[:config].empty?
|
|
93
|
+
lines
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def receiver_and_fact_lines(row)
|
|
97
|
+
lines = []
|
|
98
|
+
lines << " open_receivers: #{row[:open_receivers].join(', ')}" if row[:open_receivers].any?
|
|
99
|
+
lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
|
|
100
|
+
lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
|
|
101
|
+
lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
|
|
102
|
+
lines
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extra_surface_lines(row)
|
|
106
|
+
lines = []
|
|
107
|
+
lines << " protocol_contracts: #{row[:protocol_contracts]}" if row[:protocol_contracts].positive?
|
|
108
|
+
lines << " source_rbs_synthesizer: yes" if row[:source_rbs_synthesizer]
|
|
109
|
+
lines << " type_node_resolvers: #{row[:type_node_resolvers]}" if row[:type_node_resolvers].positive?
|
|
110
|
+
if row[:hkt_registrations].positive? || row[:hkt_definitions].positive?
|
|
111
|
+
lines << " hkt: #{row[:hkt_registrations]} registration(s), #{row[:hkt_definitions]} definition(s)"
|
|
112
|
+
end
|
|
113
|
+
lines
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def signature_paths_lines(paths)
|
|
117
|
+
lines = [" signature_paths:"]
|
|
118
|
+
paths.each do |entry|
|
|
119
|
+
marker = entry[:exists] ? "" : " (MISSING)"
|
|
120
|
+
lines << " #{entry[:path]} (#{entry[:rbs_files]} .rbs file(s))#{marker}"
|
|
121
|
+
end
|
|
122
|
+
lines
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def macro_substrate_lines(row)
|
|
126
|
+
parts = []
|
|
127
|
+
parts << "block_as_methods=#{row[:block_as_methods]}" if row[:block_as_methods].positive?
|
|
128
|
+
parts << "heredoc_templates=#{row[:heredoc_templates]}" if row[:heredoc_templates].positive?
|
|
129
|
+
parts << "trait_registries=#{row[:trait_registries]}" if row[:trait_registries].positive?
|
|
130
|
+
parts << "external_files=#{row[:external_files]}" if row[:external_files].positive?
|
|
131
|
+
return [] if parts.empty?
|
|
132
|
+
|
|
133
|
+
[" macro substrate: #{parts.join(', ')}"]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def row_json(row)
|
|
137
|
+
{
|
|
138
|
+
"gem" => row[:gem],
|
|
139
|
+
"status" => row[:status].to_s,
|
|
140
|
+
"id" => row[:id],
|
|
141
|
+
"version" => row[:version],
|
|
142
|
+
"description" => row[:description],
|
|
143
|
+
"config" => row[:config],
|
|
144
|
+
"signature_paths" => row[:signature_paths].map do |sp|
|
|
145
|
+
{ "path" => sp[:path], "exists" => sp[:exists], "rbs_files" => sp[:rbs_files] }
|
|
146
|
+
end,
|
|
147
|
+
"open_receivers" => row[:open_receivers],
|
|
148
|
+
"owns_receivers" => row[:owns_receivers],
|
|
149
|
+
"produces" => row[:produces],
|
|
150
|
+
"consumes" => row[:consumes],
|
|
151
|
+
"block_as_methods" => row[:block_as_methods],
|
|
152
|
+
"heredoc_templates" => row[:heredoc_templates],
|
|
153
|
+
"trait_registries" => row[:trait_registries],
|
|
154
|
+
"external_files" => row[:external_files],
|
|
155
|
+
"type_node_resolvers" => row[:type_node_resolvers],
|
|
156
|
+
"hkt_registrations" => row[:hkt_registrations],
|
|
157
|
+
"hkt_definitions" => row[:hkt_definitions],
|
|
158
|
+
"protocol_contracts" => row[:protocol_contracts],
|
|
159
|
+
"source_rbs_synthesizer" => row[:source_rbs_synthesizer],
|
|
160
|
+
"load_error" => row[:load_error]
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def summary
|
|
165
|
+
{
|
|
166
|
+
"total" => @rows.size,
|
|
167
|
+
"loaded" => @rows.count { |r| r[:status] == :loaded },
|
|
168
|
+
"load_error" => @rows.count { |r| r[:status] == :load_error }
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -31,7 +31,9 @@ module Rigor
|
|
|
31
31
|
"mcp" => :run_mcp,
|
|
32
32
|
"baseline" => :run_baseline,
|
|
33
33
|
"triage" => :run_triage,
|
|
34
|
-
"coverage" => :run_coverage
|
|
34
|
+
"coverage" => :run_coverage,
|
|
35
|
+
"plugins" => :run_plugins,
|
|
36
|
+
"playground" => :run_playground
|
|
35
37
|
}.freeze
|
|
36
38
|
|
|
37
39
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -120,7 +122,7 @@ module Rigor
|
|
|
120
122
|
return false
|
|
121
123
|
end
|
|
122
124
|
|
|
123
|
-
baseline = Analysis::Baseline.load(path)
|
|
125
|
+
baseline = Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
124
126
|
return false if baseline.nil? || baseline.empty?
|
|
125
127
|
|
|
126
128
|
drifted = baseline.audit(raw_diagnostics).reject { |row| row.status == :within }
|
|
@@ -163,7 +165,7 @@ module Rigor
|
|
|
163
165
|
path = resolve_baseline_path(configuration, options)
|
|
164
166
|
return result if path.nil?
|
|
165
167
|
|
|
166
|
-
baseline = Analysis::Baseline.load(path)
|
|
168
|
+
baseline = Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
167
169
|
return result if baseline.nil?
|
|
168
170
|
|
|
169
171
|
surfaced, silenced_count = baseline.filter(result.diagnostics)
|
|
@@ -474,9 +476,29 @@ module Rigor
|
|
|
474
476
|
|
|
475
477
|
File.write(path, init_template)
|
|
476
478
|
@out.puts("Created #{path}")
|
|
479
|
+
print_init_next_steps(path)
|
|
477
480
|
0
|
|
478
481
|
end
|
|
479
482
|
|
|
483
|
+
# `rigor init`'s template ships empty `plugins:` so a fresh
|
|
484
|
+
# init has nothing to validate — but the moment the user adds
|
|
485
|
+
# any plugin entry, the activation-failure surfaces enumerated
|
|
486
|
+
# in `rigor plugins`'s docstring become real. Point them at
|
|
487
|
+
# the verification command + the canonical readiness flow so
|
|
488
|
+
# silent failures (the cwd / Gemfile / signature_paths
|
|
489
|
+
# mismatches that surfaced during the Mastodon trial) get
|
|
490
|
+
# caught the first time the user wires a plugin, not the first
|
|
491
|
+
# time `rigor check` reports false positives that should have
|
|
492
|
+
# been covered.
|
|
493
|
+
def print_init_next_steps(path)
|
|
494
|
+
@out.puts ""
|
|
495
|
+
@out.puts "Next steps:"
|
|
496
|
+
@out.puts " 1. Edit #{path} — add the `plugins:` your project needs."
|
|
497
|
+
@out.puts " 2. Run `rigor plugins` to verify every configured plugin loads."
|
|
498
|
+
@out.puts " (`--strict` exits 1 on failure; ideal CI gate.)"
|
|
499
|
+
@out.puts " 3. Run `rigor check` to analyse your code."
|
|
500
|
+
end
|
|
501
|
+
|
|
480
502
|
# Renders the starter `.rigor.yml` body. The template
|
|
481
503
|
# serialises `Configuration::DEFAULTS` (so the on-disk file
|
|
482
504
|
# round-trips through `Configuration.load`) and prepends a
|
|
@@ -596,6 +618,23 @@ module Rigor
|
|
|
596
618
|
CLI::CoverageCommand.new(argv: @argv, out: @out, err: @err).run
|
|
597
619
|
end
|
|
598
620
|
|
|
621
|
+
def run_plugins
|
|
622
|
+
require_relative "cli/plugins_command"
|
|
623
|
+
|
|
624
|
+
CLI::PluginsCommand.new(argv: @argv, out: @out, err: @err).run
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def run_playground
|
|
628
|
+
begin
|
|
629
|
+
require "rigor/playground"
|
|
630
|
+
rescue LoadError
|
|
631
|
+
@err.puts "rigor playground requires the rigor-playground gem."
|
|
632
|
+
@err.puts "Install it with: gem install rigor-playground"
|
|
633
|
+
return EXIT_USAGE
|
|
634
|
+
end
|
|
635
|
+
Rigor::CLI::PlaygroundCommand.new(@argv[1..], @out, @err).run
|
|
636
|
+
end
|
|
637
|
+
|
|
599
638
|
def write_result(result, format)
|
|
600
639
|
case format
|
|
601
640
|
when "json"
|
|
@@ -641,6 +680,8 @@ module Rigor
|
|
|
641
680
|
mcp Run the Rigor MCP server over stdio (ADR-33)
|
|
642
681
|
triage Summarise diagnostics: distribution, hotspots, hints (ADR-23)
|
|
643
682
|
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
683
|
+
plugins Report activation status of every configured plugin
|
|
684
|
+
playground Start the browser playground (requires rigor-playground gem)
|
|
644
685
|
version Print the Rigor version
|
|
645
686
|
help Print this help
|
|
646
687
|
HELP
|
|
@@ -106,6 +106,8 @@ module Rigor
|
|
|
106
106
|
params_node = params_root.parameters
|
|
107
107
|
return {} if params_node.nil?
|
|
108
108
|
|
|
109
|
+
apply_auto_splat(params_node)
|
|
110
|
+
|
|
109
111
|
bindings = {}
|
|
110
112
|
bind_positionals(params_node, bindings, 0)
|
|
111
113
|
bind_rest(params_node, bindings)
|
|
@@ -115,6 +117,39 @@ module Rigor
|
|
|
115
117
|
bindings
|
|
116
118
|
end
|
|
117
119
|
|
|
120
|
+
# Ruby blocks (NOT lambdas) auto-splat a single yielded
|
|
121
|
+
# Tuple-shaped value when the block declares more than one
|
|
122
|
+
# required positional parameter:
|
|
123
|
+
#
|
|
124
|
+
# { a: 1 }.each { |k, v| ... }
|
|
125
|
+
#
|
|
126
|
+
# yields `[key, value]` as a single arg, but the two-param
|
|
127
|
+
# block sees `k = key, v = value`. RBS / IteratorDispatch
|
|
128
|
+
# encode this as the block taking ONE `[K, V]` Tuple
|
|
129
|
+
# parameter; without this fix-up the binder would assign
|
|
130
|
+
# `k = Tuple[K, V]` and `v = Dynamic[Top]`, and any call
|
|
131
|
+
# on `k.<method-not-on-Tuple>` would false-fire.
|
|
132
|
+
#
|
|
133
|
+
# The rule fires only when (a) the receiver yields exactly
|
|
134
|
+
# one value (`expected_param_types.size == 1`), (b) the
|
|
135
|
+
# block declares more than one positional slot, and (c)
|
|
136
|
+
# that single expected element is a Tuple. Multi-arg yields
|
|
137
|
+
# (e.g. `each_with_index`'s `(element, index)` pair) are
|
|
138
|
+
# NOT auto-splatted — matching Ruby semantics where a
|
|
139
|
+
# multi-arg yield to a `|a, b, c|` block fills the extra
|
|
140
|
+
# slot with nil rather than splatting any element.
|
|
141
|
+
def apply_auto_splat(params_node)
|
|
142
|
+
return unless @expected_param_types.size == 1
|
|
143
|
+
|
|
144
|
+
pos_count = params_node.requireds.size + params_node.optionals.size + params_node.posts.size
|
|
145
|
+
return unless pos_count > 1
|
|
146
|
+
|
|
147
|
+
first = @expected_param_types[0]
|
|
148
|
+
return unless first.is_a?(Type::Tuple)
|
|
149
|
+
|
|
150
|
+
@expected_param_types = first.elements
|
|
151
|
+
end
|
|
152
|
+
|
|
118
153
|
def bind_positionals(params_node, bindings, cursor)
|
|
119
154
|
cursor = bind_required_positionals(params_node, bindings, cursor)
|
|
120
155
|
cursor = bind_optional_positionals(params_node, bindings, cursor)
|
|
@@ -6,6 +6,7 @@ require_relative "../type"
|
|
|
6
6
|
require_relative "../ast"
|
|
7
7
|
require_relative "block_parameter_binder"
|
|
8
8
|
require_relative "fallback"
|
|
9
|
+
require_relative "indexed_narrowing"
|
|
9
10
|
require_relative "macro_block_self_type"
|
|
10
11
|
require_relative "method_dispatcher"
|
|
11
12
|
require_relative "narrowing"
|
|
@@ -1133,6 +1134,69 @@ module Rigor
|
|
|
1133
1134
|
type_of(body.last)
|
|
1134
1135
|
end
|
|
1135
1136
|
|
|
1137
|
+
# Indexed-collection narrowing — `receiver[key]` after a
|
|
1138
|
+
# prior `receiver[key] ||= default` reads the post-`||=`
|
|
1139
|
+
# type when the receiver and key are stable enough to
|
|
1140
|
+
# address. Sits ahead of `MethodDispatcher.dispatch` so
|
|
1141
|
+
# the standard `Hash#[]` / `Array#[]` answer (which would
|
|
1142
|
+
# fold to `Constant[nil]` for an empty `HashShape{}` or
|
|
1143
|
+
# `Tuple[]`) does not override the narrowing. See
|
|
1144
|
+
# {Inference::IndexedNarrowing}.
|
|
1145
|
+
def indexed_narrowing_for(node)
|
|
1146
|
+
IndexedNarrowing.lookup_for_call(node, scope) || method_chain_narrowing_for(node)
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
# Stable single-hop chain narrowing — `receiver.method`
|
|
1150
|
+
# after an `is_a?` / `kind_of?` / `instance_of?` predicate
|
|
1151
|
+
# established the narrowing on the dominated edge. The
|
|
1152
|
+
# call MUST be no-arg + no-block + rooted at a local-var /
|
|
1153
|
+
# ivar read; everything else falls through to the
|
|
1154
|
+
# standard dispatcher. ROADMAP § Future cycles —
|
|
1155
|
+
# "Method-call receiver narrowing across stable
|
|
1156
|
+
# receivers" — Law-of-Demeter-justified single-hop scope.
|
|
1157
|
+
def method_chain_narrowing_for(node)
|
|
1158
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
1159
|
+
return nil unless node.block.nil?
|
|
1160
|
+
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
1161
|
+
|
|
1162
|
+
case node.receiver
|
|
1163
|
+
when Prism::LocalVariableReadNode
|
|
1164
|
+
scope.method_chain_narrowing(:local, node.receiver.name, node.name)
|
|
1165
|
+
when Prism::InstanceVariableReadNode
|
|
1166
|
+
scope.method_chain_narrowing(:ivar, node.receiver.name, node.name)
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# v0.0.3 A — implicit-self calls prefer a same-named
|
|
1171
|
+
# top-level `def` over RBS dispatch. Without this,
|
|
1172
|
+
# a helper like `def select(...)` defined inside an
|
|
1173
|
+
# `RSpec.describe ... do ... end` block mis-routes
|
|
1174
|
+
# through `Enumerable#select` / `Object#select` and
|
|
1175
|
+
# the caller observes `Array[Elem]` instead of the
|
|
1176
|
+
# helper's actual return type. The check fires only
|
|
1177
|
+
# for `node.receiver.nil?` (true implicit self), so
|
|
1178
|
+
# explicit-receiver dispatch is unaffected.
|
|
1179
|
+
def try_local_def_dispatch(node, receiver, arg_types)
|
|
1180
|
+
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1181
|
+
return nil unless local_def
|
|
1182
|
+
|
|
1183
|
+
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1184
|
+
return local_inference if local_inference && adoptable_self_call_result?(local_inference)
|
|
1185
|
+
|
|
1186
|
+
# The local def matches by name but the inference was
|
|
1187
|
+
# disqualified — either the parameter shape is too complex
|
|
1188
|
+
# for the first-iteration binder (kwargs / optionals /
|
|
1189
|
+
# rest), or ADR-24 slice 1's conservative gate declined
|
|
1190
|
+
# the resolved return type inside a class body (see
|
|
1191
|
+
# `adoptable_self_call_result?`). `Dynamic[Top]` is the
|
|
1192
|
+
# safest answer: RBS dispatch would be wrong (the method
|
|
1193
|
+
# is user-defined and shadows whatever ancestor method the
|
|
1194
|
+
# dispatch would find), and `Dynamic[Top]` propagates
|
|
1195
|
+
# correctly through downstream call chains without
|
|
1196
|
+
# surfacing misleading false-positive diagnostics.
|
|
1197
|
+
dynamic_top
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1136
1200
|
# Slice 2 routes call expressions through `MethodDispatcher`. The
|
|
1137
1201
|
# receiver and every argument are typed first, then the dispatcher is
|
|
1138
1202
|
# asked for a result type. A nil result triggers the fail-soft fallback
|
|
@@ -1140,40 +1204,15 @@ module Rigor
|
|
|
1140
1204
|
# their own fallbacks for unrecognised receivers/args, so the tracer
|
|
1141
1205
|
# captures both the immediate dispatch miss and the deeper cause).
|
|
1142
1206
|
def call_type_for(node)
|
|
1207
|
+
narrowed = indexed_narrowing_for(node)
|
|
1208
|
+
return narrowed if narrowed
|
|
1209
|
+
|
|
1143
1210
|
receiver = call_receiver_type_for(node)
|
|
1144
1211
|
arg_types = call_arg_types(node)
|
|
1145
1212
|
block_type = block_return_type_for(node, receiver, arg_types)
|
|
1146
1213
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
# a helper like `def select(...)` defined inside an
|
|
1150
|
-
# `RSpec.describe ... do ... end` block mis-routes
|
|
1151
|
-
# through `Enumerable#select` / `Object#select` and
|
|
1152
|
-
# the caller observes `Array[Elem]` instead of the
|
|
1153
|
-
# helper's actual return type. The check fires only
|
|
1154
|
-
# for `node.receiver.nil?` (true implicit self), so
|
|
1155
|
-
# explicit-receiver dispatch is unaffected.
|
|
1156
|
-
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1157
|
-
if local_def
|
|
1158
|
-
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1159
|
-
return local_inference if local_inference && adoptable_self_call_result?(local_inference)
|
|
1160
|
-
|
|
1161
|
-
# The local def matches by name but the inference
|
|
1162
|
-
# was disqualified — either the parameter shape is
|
|
1163
|
-
# too complex for the first-iteration binder
|
|
1164
|
-
# (kwargs / optionals / rest), or ADR-24 slice 1's
|
|
1165
|
-
# conservative gate declined the resolved return
|
|
1166
|
-
# type inside a class body (see
|
|
1167
|
-
# `adoptable_self_call_result?`).
|
|
1168
|
-
# Returning `Dynamic[Top]` is the safest answer:
|
|
1169
|
-
# we know RBS dispatch would be wrong (the
|
|
1170
|
-
# method is user-defined and shadows whatever
|
|
1171
|
-
# ancestor method the dispatch would find), and
|
|
1172
|
-
# `Dynamic[Top]` propagates correctly through
|
|
1173
|
-
# downstream call chains without surfacing
|
|
1174
|
-
# misleading false-positive diagnostics.
|
|
1175
|
-
return dynamic_top
|
|
1176
|
-
end
|
|
1214
|
+
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1215
|
+
return local_def_result if local_def_result
|
|
1177
1216
|
|
|
1178
1217
|
# v0.0.6 phase 2 — per-element block fold for Tuple
|
|
1179
1218
|
# receivers. When `[a, b, c].map { |x| f(x) }` and the
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "mutation_widening"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Closes the "`params[:f] ||= []; params[:f] << x`" precision
|
|
11
|
+
# gap surfaced by the Redmine 6.1.2 `Query#as_params` survey
|
|
12
|
+
# (ROADMAP § Future cycles / Type-language / engine —
|
|
13
|
+
# "Indexed-collection narrowing through `Hash[k] ||= default`").
|
|
14
|
+
#
|
|
15
|
+
# After `receiver[key] ||= default` the next read at
|
|
16
|
+
# `receiver[key]` is known non-nil, but Rigor types each
|
|
17
|
+
# `Hash#[]` independently and the subsequent `<<` / `[]=` /
|
|
18
|
+
# other mutator dispatches against the un-narrowed result —
|
|
19
|
+
# which on a `HashShape{}` carrier folds to `Constant[nil]`.
|
|
20
|
+
#
|
|
21
|
+
# This module is the address-recogniser + invalidator shared
|
|
22
|
+
# by {Inference::StatementEvaluator}'s `eval_index_or_write`
|
|
23
|
+
# handler (which RECORDS the narrowing) and `eval_call`
|
|
24
|
+
# (which INVALIDATES on intervening writes / mutators) and
|
|
25
|
+
# by {Inference::ExpressionTyper}'s `call_type_for` (which
|
|
26
|
+
# CONSUMES the narrowing when typing a follow-up `[]` read).
|
|
27
|
+
#
|
|
28
|
+
# **Stable receivers.** A receiver is "stable" iff it is a
|
|
29
|
+
# `LocalVariableReadNode` or `InstanceVariableReadNode`.
|
|
30
|
+
# Method-call chains (`foo.bar[:k]`) and other shapes are
|
|
31
|
+
# rejected because a follow-up read against an identical-
|
|
32
|
+
# looking AST chain has no guarantee of resolving to the
|
|
33
|
+
# same runtime value — narrowing it would invent a fact.
|
|
34
|
+
#
|
|
35
|
+
# **Stable keys.** A key is "stable" iff it is a literal
|
|
36
|
+
# `SymbolNode` / `StringNode` / `IntegerNode`. Local-variable
|
|
37
|
+
# keys (`params[field]`) are excluded for the same
|
|
38
|
+
# invent-a-fact reason: the local could be rebound between
|
|
39
|
+
# the `||=` and the read.
|
|
40
|
+
#
|
|
41
|
+
# **Invalidation.** Three conditions drop a recorded
|
|
42
|
+
# narrowing:
|
|
43
|
+
# - The receiver variable is rebound (handled inside
|
|
44
|
+
# `Scope#with_local` / `Scope#with_ivar`).
|
|
45
|
+
# - An intervening `receiver[key] = value` writes the same
|
|
46
|
+
# slot — `:[]=` could rebind the slot to nil; conservative
|
|
47
|
+
# drop.
|
|
48
|
+
# - An intervening mutator from {MutationWidening::HASH_MUTATORS}
|
|
49
|
+
# or {MutationWidening::ARRAY_MUTATORS} runs against the
|
|
50
|
+
# receiver (e.g. `params.delete(:f)`, `params.clear`).
|
|
51
|
+
#
|
|
52
|
+
# All three are implemented in `StatementEvaluator#eval_call`'s
|
|
53
|
+
# post-dispatch path through {.invalidate_after_call}.
|
|
54
|
+
module IndexedNarrowing
|
|
55
|
+
# Literal Prism nodes whose Ruby value the analyzer trusts
|
|
56
|
+
# as a stable address. Symbol / String are the dominant
|
|
57
|
+
# Hash key shapes; Integer covers numerically-keyed Hashes
|
|
58
|
+
# and Array indices.
|
|
59
|
+
STABLE_KEY_NODES = [Prism::SymbolNode, Prism::StringNode, Prism::IntegerNode].freeze
|
|
60
|
+
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
# Returns `[receiver_kind, receiver_name]` when `node` is a
|
|
64
|
+
# `LocalVariableReadNode` or `InstanceVariableReadNode`,
|
|
65
|
+
# otherwise nil.
|
|
66
|
+
def stable_receiver(node)
|
|
67
|
+
case node
|
|
68
|
+
when Prism::LocalVariableReadNode then [:local, node.name]
|
|
69
|
+
when Prism::InstanceVariableReadNode then [:ivar, node.name]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the literal Ruby value when `node` is a stable
|
|
74
|
+
# key shape, otherwise nil. Symbols → `Symbol`,
|
|
75
|
+
# Strings → `String` (unescaped), Integers → `Integer`.
|
|
76
|
+
def stable_key(node)
|
|
77
|
+
case node
|
|
78
|
+
when Prism::SymbolNode then node.unescaped.to_sym
|
|
79
|
+
when Prism::StringNode then node.unescaped
|
|
80
|
+
when Prism::IntegerNode then node.value
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns `[receiver_kind, receiver_name, key]` when the
|
|
85
|
+
# CallNode is a `receiver[key]` read or write whose
|
|
86
|
+
# receiver and key are both stable, otherwise nil. Used
|
|
87
|
+
# by both the recorder (for `IndexOrWriteNode`'s
|
|
88
|
+
# receiver/arguments triplet) and the invalidator (for
|
|
89
|
+
# `CallNode :[]=` / mutator calls). Treats only the FIRST
|
|
90
|
+
# argument as the key; `:[]=`'s second argument is the
|
|
91
|
+
# rvalue and is not part of the address.
|
|
92
|
+
def stable_address(receiver_node, key_node)
|
|
93
|
+
receiver = stable_receiver(receiver_node)
|
|
94
|
+
return nil if receiver.nil?
|
|
95
|
+
|
|
96
|
+
key = stable_key(key_node)
|
|
97
|
+
return nil if key.nil?
|
|
98
|
+
|
|
99
|
+
[receiver.first, receiver.last, key]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Looks up a recorded narrowing for `receiver[key]` against
|
|
103
|
+
# `scope`, returning the narrowed type or nil when no
|
|
104
|
+
# entry applies. Used by ExpressionTyper's `[]` dispatch
|
|
105
|
+
# to refine the result of a stable indexed read.
|
|
106
|
+
def lookup_for_call(node, scope)
|
|
107
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
108
|
+
return nil unless node.name == :[]
|
|
109
|
+
return nil if node.arguments.nil?
|
|
110
|
+
return nil unless node.arguments.arguments.size == 1
|
|
111
|
+
|
|
112
|
+
address = stable_address(node.receiver, node.arguments.arguments.first)
|
|
113
|
+
return nil if address.nil?
|
|
114
|
+
|
|
115
|
+
scope.indexed_narrowing(*address)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Removes recorded narrowings invalidated by `call_node`.
|
|
119
|
+
# Two patterns:
|
|
120
|
+
#
|
|
121
|
+
# - `receiver[key] = value` (a `:[]=` against a stable
|
|
122
|
+
# address): drop the specific `(receiver, key)` entry.
|
|
123
|
+
# - Any mutator from `HASH_MUTATORS` / `ARRAY_MUTATORS`
|
|
124
|
+
# against a stable receiver: drop EVERY entry rooted
|
|
125
|
+
# at that receiver, because the mutator could rebind
|
|
126
|
+
# any slot.
|
|
127
|
+
#
|
|
128
|
+
# Returns the updated scope. Always-safe (only forgets;
|
|
129
|
+
# never invents).
|
|
130
|
+
def invalidate_after_call(call_node:, current_scope:)
|
|
131
|
+
return current_scope unless call_node.is_a?(Prism::CallNode)
|
|
132
|
+
|
|
133
|
+
if call_node.name == :[]=
|
|
134
|
+
invalidate_indexed_write(call_node, current_scope)
|
|
135
|
+
elsif mutator?(call_node.name)
|
|
136
|
+
invalidate_mutator(call_node, current_scope)
|
|
137
|
+
else
|
|
138
|
+
current_scope
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def mutator?(method_name)
|
|
143
|
+
MutationWidening::HASH_MUTATORS.include?(method_name) ||
|
|
144
|
+
MutationWidening::ARRAY_MUTATORS.include?(method_name)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def invalidate_indexed_write(call_node, current_scope)
|
|
148
|
+
args = call_node.arguments&.arguments
|
|
149
|
+
return current_scope if args.nil? || args.empty?
|
|
150
|
+
|
|
151
|
+
address = stable_address(call_node.receiver, args.first)
|
|
152
|
+
return current_scope if address.nil?
|
|
153
|
+
|
|
154
|
+
current_scope.without_indexed_narrowing(*address)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def invalidate_mutator(call_node, current_scope)
|
|
158
|
+
receiver = stable_receiver(call_node.receiver)
|
|
159
|
+
return current_scope if receiver.nil?
|
|
160
|
+
|
|
161
|
+
current_scope.without_indexed_narrowings_for(*receiver)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Companion invalidator for single-hop method-chain
|
|
165
|
+
# narrowings (ROADMAP § Future cycles — "Method-call
|
|
166
|
+
# receiver narrowing across stable receivers", B2 from
|
|
167
|
+
# the slice's design notes). Drops every
|
|
168
|
+
# `(receiver, *)` chain narrowing rooted at the call's
|
|
169
|
+
# OUTER stable receiver — matching the ROADMAP's "any
|
|
170
|
+
# intervening method call against the same receiver"
|
|
171
|
+
# criterion. A call against `x.last` (the OUTER receiver
|
|
172
|
+
# is a `CallNode`, not a stable root) does NOT drop
|
|
173
|
+
# narrowings keyed on `x`, so the worked-site
|
|
174
|
+
# `x.last << y` pattern correctly preserves the chain
|
|
175
|
+
# narrowing for any further `x.last` read in the same
|
|
176
|
+
# body. Always-safe (only forgets; never invents).
|
|
177
|
+
def invalidate_chain_after_call(call_node:, current_scope:)
|
|
178
|
+
return current_scope unless call_node.is_a?(Prism::CallNode)
|
|
179
|
+
|
|
180
|
+
receiver = stable_receiver(call_node.receiver)
|
|
181
|
+
return current_scope if receiver.nil?
|
|
182
|
+
|
|
183
|
+
current_scope.without_method_chain_narrowings_for(*receiver)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -42,9 +42,33 @@ module Rigor
|
|
|
42
42
|
when :inject, :reduce then inject_block_params(receiver, args)
|
|
43
43
|
when :group_by, :partition then single_element_block_params(receiver)
|
|
44
44
|
when :each_slice, :each_cons then slice_block_params(receiver)
|
|
45
|
+
when :new then class_new_block_params(receiver, args)
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
# `Class.new { |c| … }` and `Class.new(Parent) { |c| … }`
|
|
50
|
+
# — the block parameter is the freshly-created anonymous
|
|
51
|
+
# class, statically representable as the parent's singleton
|
|
52
|
+
# type (the new class inherits every singleton method the
|
|
53
|
+
# parent exposes, which is what callers use this form to
|
|
54
|
+
# configure: `c.table_name = …`, `c.attribute :foo`, etc.).
|
|
55
|
+
# No parent → `singleton(Object)`. RBS would otherwise widen
|
|
56
|
+
# the block param to bare `Nominal[Class]`, dropping access
|
|
57
|
+
# to the parent's class-side surface.
|
|
58
|
+
def class_new_block_params(receiver, args)
|
|
59
|
+
return nil unless class_metaclass_receiver?(receiver)
|
|
60
|
+
|
|
61
|
+
parent = args.first
|
|
62
|
+
return [Type::Combinator.singleton_of("Object")] if parent.nil?
|
|
63
|
+
return [parent] if parent.is_a?(Type::Singleton)
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def class_metaclass_receiver?(type)
|
|
69
|
+
type.is_a?(Type::Singleton) && type.class_name == "Class"
|
|
70
|
+
end
|
|
71
|
+
|
|
48
72
|
def times_block_params(receiver)
|
|
49
73
|
return nil unless integer_rooted?(receiver)
|
|
50
74
|
|