rigortype 0.4.3 → 0.4.5
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 +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -37,8 +37,11 @@ module Rigor
|
|
|
37
37
|
# returned instead of the 1:1 mirror.
|
|
38
38
|
# @return absolute path of the target `.rbs`
|
|
39
39
|
# file for the candidate.
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
#
|
|
41
|
+
# A member the signature tree already declares (`method_name` / `kind`, ADR-122 WD2) is routed to the file
|
|
42
|
+
# that declares it, even when another file declares its class first (#1488).
|
|
43
|
+
def target_for(source_path, class_name: nil, method_name: nil, kind: nil)
|
|
44
|
+
existing = existing_member_target_for(class_name, method_name, kind) || existing_target_for(class_name)
|
|
42
45
|
return existing if existing
|
|
43
46
|
|
|
44
47
|
rel_to_root = source_relative_to_root(source_path)
|
|
@@ -46,6 +49,19 @@ module Rigor
|
|
|
46
49
|
sig_root_dir / "#{stripped.sub_ext('')}.rbs"
|
|
47
50
|
end
|
|
48
51
|
|
|
52
|
+
# `kind` may be `:singleton_instance` (a `def self?.` candidate), which routes to whichever file declares
|
|
53
|
+
# either half.
|
|
54
|
+
def existing_member_target_for(class_name, method_name, kind)
|
|
55
|
+
return nil if class_name.nil? || method_name.nil? || @layout_index.nil?
|
|
56
|
+
|
|
57
|
+
kinds = kind == :singleton_instance ? %i[instance singleton] : [kind]
|
|
58
|
+
kinds.each do |k|
|
|
59
|
+
file = @layout_index.member_file_for(class_name, method_name, k)
|
|
60
|
+
return file if file
|
|
61
|
+
end
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
49
65
|
def existing_target_for(class_name)
|
|
50
66
|
return nil if class_name.nil? || @layout_index.nil?
|
|
51
67
|
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
require_relative "classification"
|
|
6
6
|
require_relative "superclass_spelling"
|
|
7
|
+
require_relative "write_reporter"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module SigGen
|
|
@@ -26,9 +27,11 @@ module Rigor
|
|
|
26
27
|
# @param selection — subset of
|
|
27
28
|
# {Classification} constants to include; an empty
|
|
28
29
|
# array means "all emittable classifications".
|
|
29
|
-
|
|
30
|
+
# @param observation — `{ paths:, missing: }` under `--params=observed`, else `nil`; JSON carries it
|
|
31
|
+
# as the top-level `observation` key (#1398).
|
|
32
|
+
def render(candidates:, mode:, format:, selection:, observation: nil)
|
|
30
33
|
case format
|
|
31
|
-
when "json" then render_json(filter(candidates, selection, with_skipped: true))
|
|
34
|
+
when "json" then render_json(filter(candidates, selection, with_skipped: true), observation)
|
|
32
35
|
when "text"
|
|
33
36
|
filtered = filter(candidates, selection)
|
|
34
37
|
mode == :diff ? render_diff(filtered) : render_print(filtered)
|
|
@@ -121,175 +124,30 @@ module Rigor
|
|
|
121
124
|
end
|
|
122
125
|
end
|
|
123
126
|
|
|
124
|
-
def render_json(candidates)
|
|
127
|
+
def render_json(candidates, observation = nil)
|
|
125
128
|
payload = { candidates: candidates.map(&:to_h) }
|
|
129
|
+
payload[:observation] = observation if observation
|
|
126
130
|
@out.puts(JSON.pretty_generate(payload))
|
|
127
131
|
end
|
|
128
132
|
|
|
129
133
|
public
|
|
130
134
|
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
# @param refused — methods refused as `sig.skipped.inline-differs`; the JSON payload names them under
|
|
134
|
-
# `refused` (absent when there are none, so an ordinary payload is unchanged), and text mode leaves them
|
|
135
|
-
# to the command's stderr `REFUSED` lines.
|
|
135
|
+
# The per-source-file outcomes of a `--write` run, and the `--check` verdict, are reported by
|
|
136
|
+
# {WriteReporter}; these delegate so the command keeps one renderer.
|
|
136
137
|
def render_write(results:, format:, refused: [])
|
|
137
|
-
|
|
138
|
-
when "json" then render_write_json(results, refused)
|
|
139
|
-
when "text" then render_write_text(results)
|
|
140
|
-
else raise ArgumentError, "unsupported format: #{format}"
|
|
141
|
-
end
|
|
138
|
+
WriteReporter.new(out: @out).render_write(results: results, format: format, refused: refused)
|
|
142
139
|
end
|
|
143
140
|
|
|
144
|
-
# ADR-112 WD4 — `sig-gen --check`: the results of a dry-run `--write`. Only the targets `--write` would
|
|
145
|
-
# change (or refuse) are shown, each with the lines it would add; the verdict is the exit status, which
|
|
146
|
-
# the command derives from the same results ({.out_of_date}).
|
|
147
|
-
#
|
|
148
|
-
# @param refused — the methods the generator refused to reconcile (`sig.skipped.inline-differs`):
|
|
149
|
-
# `sig/` is not up to date while one stands, and `--write` cannot fix it.
|
|
150
141
|
def render_check(results:, format:, refused: [])
|
|
151
|
-
|
|
152
|
-
case format
|
|
153
|
-
when "json"
|
|
154
|
-
@out.puts(JSON.pretty_generate({ up_to_date: stale.empty? && refused.empty?,
|
|
155
|
-
results: stale.map { |r| check_entry(r) },
|
|
156
|
-
refused: refused.map(&:to_h) }))
|
|
157
|
-
when "text" then render_check_text(stale, refused)
|
|
158
|
-
else raise ArgumentError, "unsupported format: #{format}"
|
|
159
|
-
end
|
|
142
|
+
WriteReporter.new(out: @out).render_check(results: results, format: format, refused: refused)
|
|
160
143
|
end
|
|
161
144
|
|
|
162
|
-
# One line per method sig-gen refused to reconcile with its `sig/` copy. Shared by `--write` (on stderr,
|
|
163
|
-
# next to the write report) and `--check`.
|
|
164
145
|
def self.refusal_lines(refused)
|
|
165
|
-
refused
|
|
166
|
-
separator = candidate.kind == :singleton ? "." : "#"
|
|
167
|
-
"REFUSED #{candidate.path}: #{candidate.class_name}#{separator}#{candidate.method_name} — its inline " \
|
|
168
|
-
"declaration and its sig/ declaration disagree, so neither was changed " \
|
|
169
|
-
"(#{Classification::SKIP_DIAGNOSTIC_IDS.fetch(candidate.skip_reason)}). Make them agree, or pass " \
|
|
170
|
-
"--overwrite to replace the sig/ member with the inline declaration; `rigor explain " \
|
|
171
|
-
"#{Classification::SKIP_DIAGNOSTIC_IDS.fetch(candidate.skip_reason)}` says when that is not enough."
|
|
172
|
-
end
|
|
146
|
+
WriteReporter.refusal_lines(refused)
|
|
173
147
|
end
|
|
174
148
|
|
|
175
|
-
# The results that make a `--check` fail: a target `--write` would create or change, and one it would
|
|
176
|
-
# refuse, since a write that cannot happen is not an up-to-date `sig/` either.
|
|
177
149
|
def self.out_of_date(results)
|
|
178
|
-
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Nothing was written, so the entry must not read like one that was: `created` / `updated` become
|
|
182
|
-
# `would_create` / `would_update`. A refusal keeps its action — `--write` would refuse the same way.
|
|
183
|
-
CHECK_ACTIONS = { created: "would_create", updated: "would_update" }.freeze
|
|
184
|
-
private_constant :CHECK_ACTIONS
|
|
185
|
-
|
|
186
|
-
private
|
|
187
|
-
|
|
188
|
-
def check_entry(result)
|
|
189
|
-
entry = result.to_h
|
|
190
|
-
entry[:action] = CHECK_ACTIONS.fetch(result.action, entry[:action])
|
|
191
|
-
entry
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def render_check_text(stale, refused)
|
|
195
|
-
if stale.empty? && refused.empty?
|
|
196
|
-
@out.puts("sig/ is up to date")
|
|
197
|
-
return
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
self.class.refusal_lines(refused).each { |line| @out.puts(line) }
|
|
201
|
-
|
|
202
|
-
stale.each do |result|
|
|
203
|
-
case result.action
|
|
204
|
-
when :created, :updated then render_check_change(result)
|
|
205
|
-
when :skipped_invalid_rbs then render_write_invalid(result)
|
|
206
|
-
when :skipped_invalid_encoding then render_write_invalid_encoding(result)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def render_check_change(result)
|
|
212
|
-
counts = result.action == :created ? "#{result.applied.size} method(s)" : applied_counts(result)
|
|
213
|
-
verb = result.action == :created ? "would create" : "would update"
|
|
214
|
-
@out.puts("#{verb} #{result.target_path} (#{counts})")
|
|
215
|
-
result.applied.each do |candidate|
|
|
216
|
-
@out.puts(" - #{candidate.declared_rbs}") if candidate.declared_rbs
|
|
217
|
-
candidate.rbs_lines.each { |line| @out.puts(" + #{line}") }
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def render_write_text(results)
|
|
222
|
-
if results.all? { |r| r.action == :noop }
|
|
223
|
-
@out.puts("No changes")
|
|
224
|
-
return
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
results.each do |result|
|
|
228
|
-
case result.action
|
|
229
|
-
when :created then render_write_created(result)
|
|
230
|
-
when :updated then render_write_updated(result)
|
|
231
|
-
when :skipped_outside_sig_root then render_write_skipped(result)
|
|
232
|
-
when :skipped_invalid_rbs then render_write_invalid(result)
|
|
233
|
-
when :skipped_invalid_encoding then render_write_invalid_encoding(result)
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# `+N` counts added lines; an existing line replaced (`--overwrite`, or an inline update) is counted apart,
|
|
239
|
-
# because "added 2" when one of them rewrote a line the project already had would understate the change.
|
|
240
|
-
def applied_counts(result)
|
|
241
|
-
added = "+#{result.applied.size - result.replaced.size}"
|
|
242
|
-
result.replaced.empty? ? added : "#{added}, replaced #{result.replaced.size}"
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def render_write_created(result)
|
|
246
|
-
@out.puts("created #{result.target_path} (#{result.applied.size} method(s))")
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def render_write_updated(result)
|
|
250
|
-
@out.puts("updated #{result.target_path} (#{applied_counts(result)}, " \
|
|
251
|
-
"skipped #{result.skipped.size} user-authored)")
|
|
252
|
-
render_left_unreadable(result)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# ADR-103 WD9 — the declarations whose annotation region the writer refused to rewrite. Named per
|
|
256
|
-
# method rather than counted: the fix is a human reading one existing annotation and deciding what
|
|
257
|
-
# it should say, and there is no count of those a reader could act on.
|
|
258
|
-
def render_left_unreadable(result)
|
|
259
|
-
return if result.left_unreadable.empty?
|
|
260
|
-
|
|
261
|
-
@out.puts(" left #{result.left_unreadable.size} existing annotation(s) byte-untouched " \
|
|
262
|
-
"(sig.effect.left-unreadable):")
|
|
263
|
-
result.left_unreadable.each do |candidate|
|
|
264
|
-
@out.puts(" #{candidate.class_name}##{candidate.method_name} — " \
|
|
265
|
-
"would have emitted #{candidate.annotations.join(' ')}")
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def render_write_skipped(result)
|
|
270
|
-
@out.puts("skipped #{result.source_path} -> #{result.target_path} (outside sig root)")
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# The assembled file does not parse, so it was NOT written. Writing it would poison the sig tree — the
|
|
274
|
-
# consumer quarantines an unparseable `.rbs` whole, taking every other type in that file down with it,
|
|
275
|
-
# including the user's own hand-written ones.
|
|
276
|
-
def render_write_invalid(result)
|
|
277
|
-
@out.puts("REFUSED #{result.target_path} — the generated RBS does not parse, so it was not written")
|
|
278
|
-
@out.puts(" #{result.error}")
|
|
279
|
-
@out.puts(" This is a bug in Rigor's RBS rendering, not in your code — please report it.")
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# The EXISTING target file is not valid UTF-8, so the writer will not merge into it — the opposite
|
|
283
|
-
# attribution from {#render_write_invalid}: this one is the file's problem, and the fix is the user's.
|
|
284
|
-
def render_write_invalid_encoding(result)
|
|
285
|
-
@out.puts("REFUSED #{result.target_path} — the existing file is not valid UTF-8, so it was not updated")
|
|
286
|
-
@out.puts(" Re-save the file as UTF-8 and re-run; sig-gen never modifies a file it cannot read faithfully.")
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def render_write_json(results, refused)
|
|
290
|
-
payload = { results: results.map(&:to_h) }
|
|
291
|
-
payload[:refused] = refused.map(&:to_h) unless refused.empty?
|
|
292
|
-
@out.puts(JSON.pretty_generate(payload))
|
|
150
|
+
WriteReporter.out_of_date(results)
|
|
293
151
|
end
|
|
294
152
|
end
|
|
295
153
|
end
|
|
@@ -147,6 +147,17 @@ module Rigor
|
|
|
147
147
|
"not reported.",
|
|
148
148
|
next_step: "Nothing is required. To type the method, move it into a module (`module_function` or " \
|
|
149
149
|
"`extend self` keeps the call sites), or write the `Object` declaration by hand in `sig/`."
|
|
150
|
+
),
|
|
151
|
+
"sig.skipped.uncertain-module-function" => Entry.new(
|
|
152
|
+
id: "sig.skipped.uncertain-module-function",
|
|
153
|
+
summary: "Whether the method is a module function depends on a visibility call that may not run.",
|
|
154
|
+
explanation: "A bare `module_function` makes the module's later methods module functions, and a later " \
|
|
155
|
+
"bare `public`, `private` or `protected` ends that. Here such a call sits in control " \
|
|
156
|
+
"flow or a block between the two, so it may or may not run, and the method is either a " \
|
|
157
|
+
"module function (`def self?.name`) or an instance method only (`def name`). sig-gen " \
|
|
158
|
+
"writes neither rather than guess (#1968).",
|
|
159
|
+
next_step: "Move the visibility call to the module body's own statements, or name the methods it " \
|
|
160
|
+
"should cover (`public :name`, `module_function :name`), so the reading is certain."
|
|
150
161
|
)
|
|
151
162
|
}.freeze
|
|
152
163
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "classification"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module SigGen
|
|
9
|
+
# Reports a `rigor sig-gen --write` run (the per-target {WriteResult}s) and the `--check` verdict. Split out of
|
|
10
|
+
# {Renderer}, which formats candidates; this formats what the writer did with them.
|
|
11
|
+
class WriteReporter
|
|
12
|
+
def initialize(out:)
|
|
13
|
+
@out = out
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Renders the per-source-file outcomes of a `--write` run. Distinct from {Renderer#render} because the write
|
|
17
|
+
# path's reporting surface is action-oriented (created / updated / skipped) rather than candidate-oriented.
|
|
18
|
+
# @param refused — methods refused as `sig.skipped.inline-differs`; the JSON payload names them under
|
|
19
|
+
# `refused` (absent when there are none, so an ordinary payload is unchanged), and text mode leaves them
|
|
20
|
+
# to the command's stderr `REFUSED` lines.
|
|
21
|
+
def render_write(results:, format:, refused: [])
|
|
22
|
+
case format
|
|
23
|
+
when "json" then render_write_json(results, refused)
|
|
24
|
+
when "text" then render_write_text(results)
|
|
25
|
+
else raise ArgumentError, "unsupported format: #{format}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# ADR-112 WD4 — `sig-gen --check`: the results of a dry-run `--write`. Only the targets `--write` would
|
|
30
|
+
# change (or refuse) are shown, each with the lines it would add; the verdict is the exit status, which
|
|
31
|
+
# the command derives from the same results ({.out_of_date}).
|
|
32
|
+
#
|
|
33
|
+
# @param refused — the methods the generator refused to reconcile (`sig.skipped.inline-differs`):
|
|
34
|
+
# `sig/` is not up to date while one stands, and `--write` cannot fix it.
|
|
35
|
+
def render_check(results:, format:, refused: [])
|
|
36
|
+
stale = self.class.out_of_date(results)
|
|
37
|
+
case format
|
|
38
|
+
when "json"
|
|
39
|
+
@out.puts(JSON.pretty_generate({ up_to_date: stale.empty? && refused.empty?,
|
|
40
|
+
results: stale.map { |r| check_entry(r) },
|
|
41
|
+
refused: refused.map(&:to_h) }))
|
|
42
|
+
when "text" then render_check_text(stale, refused)
|
|
43
|
+
else raise ArgumentError, "unsupported format: #{format}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# One line per method sig-gen refused to reconcile with its `sig/` copy. Shared by `--write` (on stderr,
|
|
48
|
+
# next to the write report) and `--check`.
|
|
49
|
+
def self.refusal_lines(refused)
|
|
50
|
+
refused.map do |candidate|
|
|
51
|
+
separator = candidate.kind == :singleton ? "." : "#"
|
|
52
|
+
"REFUSED #{candidate.path}: #{candidate.class_name}#{separator}#{candidate.method_name} — its inline " \
|
|
53
|
+
"declaration and its sig/ declaration disagree, so neither was changed " \
|
|
54
|
+
"(#{Classification::SKIP_DIAGNOSTIC_IDS.fetch(candidate.skip_reason)}). Make them agree, or pass " \
|
|
55
|
+
"--overwrite to replace the sig/ member with the inline declaration; `rigor explain " \
|
|
56
|
+
"#{Classification::SKIP_DIAGNOSTIC_IDS.fetch(candidate.skip_reason)}` says when that is not enough."
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The results that make a `--check` fail: a target `--write` would create or change, and one it would
|
|
61
|
+
# refuse, since a write that cannot happen is not an up-to-date `sig/` either.
|
|
62
|
+
def self.out_of_date(results)
|
|
63
|
+
results.reject { |result| %i[noop skipped_outside_sig_root].include?(result.action) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Nothing was written, so the entry must not read like one that was: `created` / `updated` become
|
|
67
|
+
# `would_create` / `would_update`. A refusal keeps its action — `--write` would refuse the same way.
|
|
68
|
+
CHECK_ACTIONS = { created: "would_create", updated: "would_update" }.freeze
|
|
69
|
+
private_constant :CHECK_ACTIONS
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def check_entry(result)
|
|
74
|
+
entry = result.to_h
|
|
75
|
+
entry[:action] = CHECK_ACTIONS.fetch(result.action, entry[:action])
|
|
76
|
+
entry
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_check_text(stale, refused)
|
|
80
|
+
if stale.empty? && refused.empty?
|
|
81
|
+
@out.puts("sig/ is up to date")
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
self.class.refusal_lines(refused).each { |line| @out.puts(line) }
|
|
86
|
+
|
|
87
|
+
stale.each do |result|
|
|
88
|
+
case result.action
|
|
89
|
+
when :created, :updated then render_check_change(result)
|
|
90
|
+
when :skipped_invalid_rbs then render_write_invalid(result)
|
|
91
|
+
when :skipped_invalid_encoding then render_write_invalid_encoding(result)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render_check_change(result)
|
|
97
|
+
counts = result.action == :created ? "#{result.applied.size} method(s)" : applied_counts(result)
|
|
98
|
+
verb = result.action == :created ? "would create" : "would update"
|
|
99
|
+
@out.puts("#{verb} #{result.target_path} (#{counts})")
|
|
100
|
+
result.applied.each do |candidate|
|
|
101
|
+
@out.puts(" - #{candidate.declared_rbs}") if candidate.declared_rbs
|
|
102
|
+
candidate.rbs_lines.each { |line| @out.puts(" + #{line}") }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_write_text(results)
|
|
107
|
+
if results.all? { |r| r.action == :noop && r.skipped.empty? }
|
|
108
|
+
@out.puts("No changes")
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
results.each do |result|
|
|
113
|
+
case result.action
|
|
114
|
+
when :created then render_write_created(result)
|
|
115
|
+
when :updated then render_write_updated(result)
|
|
116
|
+
when :noop then render_write_unchanged(result)
|
|
117
|
+
when :skipped_outside_sig_root then render_write_skipped(result)
|
|
118
|
+
when :skipped_invalid_rbs then render_write_invalid(result)
|
|
119
|
+
when :skipped_invalid_encoding then render_write_invalid_encoding(result)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# `+N` counts added lines; an existing line replaced (`--overwrite`, or an inline update) is counted apart,
|
|
125
|
+
# because "added 2" when one of them rewrote a line the project already had would understate the change.
|
|
126
|
+
def applied_counts(result)
|
|
127
|
+
added = "+#{result.applied.size - result.replaced.size}"
|
|
128
|
+
result.replaced.empty? ? added : "#{added}, replaced #{result.replaced.size}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def render_write_created(result)
|
|
132
|
+
@out.puts("created #{result.target_path} (#{result.applied.size} method(s))")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def render_write_updated(result)
|
|
136
|
+
@out.puts("updated #{result.target_path} (#{applied_counts(result)}, " \
|
|
137
|
+
"skipped #{result.skipped.size} user-authored)")
|
|
138
|
+
render_write_skips(result)
|
|
139
|
+
render_left_unreadable(result)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A target the writer opened and changed nothing in, because every proposal for it was a member the
|
|
143
|
+
# project already declares (#1975). Without this line the run read as "No changes" or said nothing about it.
|
|
144
|
+
def render_write_unchanged(result)
|
|
145
|
+
return if result.skipped.empty?
|
|
146
|
+
|
|
147
|
+
@out.puts("unchanged #{result.target_path} (skipped #{result.skipped.size} user-authored)")
|
|
148
|
+
render_write_skips(result)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# One line per proposal the writer declined, so a change `--diff` or `--print` showed is never dropped
|
|
152
|
+
# without a reason. The same entries are the `skipped` rows of the `--format=json` payload.
|
|
153
|
+
def render_write_skips(result)
|
|
154
|
+
result.skipped.each do |candidate, reason|
|
|
155
|
+
separator = candidate.kind == :singleton ? "." : "#"
|
|
156
|
+
label = "#{candidate.class_name}#{separator}#{candidate.method_name}"
|
|
157
|
+
@out.puts(" skipped #{label} (#{reason.to_s.tr('_', '-')}): #{candidate.rbs_lines.join(' ')}")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# ADR-103 WD9 — the declarations whose annotation region the writer refused to rewrite. Named per
|
|
162
|
+
# method rather than counted: the fix is a human reading one existing annotation and deciding what
|
|
163
|
+
# it should say, and there is no count of those a reader could act on.
|
|
164
|
+
def render_left_unreadable(result)
|
|
165
|
+
return if result.left_unreadable.empty?
|
|
166
|
+
|
|
167
|
+
@out.puts(" left #{result.left_unreadable.size} existing annotation(s) byte-untouched " \
|
|
168
|
+
"(sig.effect.left-unreadable):")
|
|
169
|
+
result.left_unreadable.each do |candidate|
|
|
170
|
+
@out.puts(" #{candidate.class_name}##{candidate.method_name} — " \
|
|
171
|
+
"would have emitted #{candidate.annotations.join(' ')}")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_write_skipped(result)
|
|
176
|
+
@out.puts("skipped #{result.source_path} -> #{result.target_path} (outside sig root)")
|
|
177
|
+
render_write_skips(result)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The assembled file does not parse, so it was NOT written. Writing it would poison the sig tree — the
|
|
181
|
+
# consumer quarantines an unparseable `.rbs` whole, taking every other type in that file down with it,
|
|
182
|
+
# including the user's own hand-written ones.
|
|
183
|
+
def render_write_invalid(result)
|
|
184
|
+
@out.puts("REFUSED #{result.target_path} — the generated RBS does not parse, so it was not written")
|
|
185
|
+
@out.puts(" #{result.error}")
|
|
186
|
+
@out.puts(" This is a bug in Rigor's RBS rendering, not in your code — please report it.")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The EXISTING target file is not valid UTF-8, so the writer will not merge into it — the opposite
|
|
190
|
+
# attribution from {#render_write_invalid}: this one is the file's problem, and the fix is the user's.
|
|
191
|
+
def render_write_invalid_encoding(result)
|
|
192
|
+
@out.puts("REFUSED #{result.target_path} — the existing file is not valid UTF-8, so it was not updated")
|
|
193
|
+
@out.puts(" Re-save the file as UTF-8 and re-run; sig-gen never modifies a file it cannot read faithfully.")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def render_write_json(results, refused)
|
|
197
|
+
payload = { results: results.map(&:to_h) }
|
|
198
|
+
payload[:refused] = refused.map(&:to_h) unless refused.empty?
|
|
199
|
+
@out.puts(JSON.pretty_generate(payload))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -16,7 +16,9 @@ module Rigor
|
|
|
16
16
|
# - `replaced` — the subset of `applied` that replaced an existing declaration rather than adding one
|
|
17
17
|
# (`--overwrite`'s tighter returns, and inline updates).
|
|
18
18
|
# - `skipped` — the {MethodCandidate}s the writer declined (e.g. tighter-return without `--overwrite`). Each
|
|
19
|
-
# entry pairs the candidate with a skip reason keyword
|
|
19
|
+
# entry pairs the candidate with a skip reason keyword: `:user_authored` (the target already declares the
|
|
20
|
+
# member, and the writer does not replace it) or `:outside_sig_root` (the member routes to a file outside
|
|
21
|
+
# the writer's sig root, so nothing was attempted).
|
|
20
22
|
# - `left_unreadable` — the {MethodCandidate}s whose effect annotation was NOT written because the
|
|
21
23
|
# target declaration already carries annotations, so its bytes were left alone
|
|
22
24
|
# (`sig.effect.left-unreadable`; see {Writer#splice_annotations}). The signature line itself was
|
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "rbs"
|
|
5
5
|
|
|
6
6
|
require_relative "classification"
|
|
7
|
+
require_relative "layout_index"
|
|
7
8
|
require_relative "superclass_spelling"
|
|
8
9
|
require_relative "write_result"
|
|
9
10
|
|
|
@@ -62,8 +63,10 @@ module Rigor
|
|
|
62
63
|
|
|
63
64
|
@global_namespace_kinds = build_namespace_kinds(candidates)
|
|
64
65
|
@global_superclasses = build_superclasses(candidates)
|
|
65
|
-
emittable.group_by
|
|
66
|
-
|
|
66
|
+
grouped = emittable.group_by do |c|
|
|
67
|
+
@path_mapper.target_for(c.path, class_name: c.class_name, method_name: c.method_name, kind: c.rbs_kind)
|
|
68
|
+
end
|
|
69
|
+
grouped.map { |target, group| write_target(target, group) }
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
# @param candidates — only
|
|
@@ -95,7 +98,8 @@ module Rigor
|
|
|
95
98
|
source_path ||= candidates.first&.path
|
|
96
99
|
unless inside_sig_root?(target)
|
|
97
100
|
return WriteResult.new(source_path: source_path, target_path: target,
|
|
98
|
-
action: :skipped_outside_sig_root
|
|
101
|
+
action: :skipped_outside_sig_root,
|
|
102
|
+
skipped: candidates.map { |c| [c, :outside_sig_root] })
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
|
|
@@ -562,7 +566,10 @@ module Rigor
|
|
|
562
566
|
state.applied.concat(methods)
|
|
563
567
|
insert_namespace_chain(state, class_name, kinds, supers, methods)
|
|
564
568
|
else
|
|
565
|
-
|
|
569
|
+
# A class reopened in one file answers for every member of every opening, not just the first one's
|
|
570
|
+
# (#1488): a member another opening declares is user-authored, not new.
|
|
571
|
+
declared = find_class_decls(state.decls, class_name).flat_map { |d| collect_member_pairs(d) }
|
|
572
|
+
state.source = merge_into_existing_class(state.source, decl, methods, state, declared)
|
|
566
573
|
state.decls = parse_signature(state.source) || state.decls
|
|
567
574
|
end
|
|
568
575
|
end
|
|
@@ -589,12 +596,25 @@ module Rigor
|
|
|
589
596
|
nil
|
|
590
597
|
end
|
|
591
598
|
|
|
599
|
+
# Every declaration of `qualified_name` in the file, in {#find_class_decl}'s order (so the first is the one
|
|
600
|
+
# it returns).
|
|
601
|
+
def find_class_decls(decls, qualified_name, prefix = [], found = [])
|
|
602
|
+
decls.each do |decl|
|
|
603
|
+
next unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
|
|
604
|
+
|
|
605
|
+
local = decl.name.to_s.sub(/\A::/, "")
|
|
606
|
+
full = prefix.empty? ? local : "#{prefix.join('::')}::#{local}"
|
|
607
|
+
found << decl if full == qualified_name
|
|
608
|
+
find_class_decls(decl.members, qualified_name, prefix + [local], found)
|
|
609
|
+
end
|
|
610
|
+
found
|
|
611
|
+
end
|
|
612
|
+
|
|
592
613
|
def ends_with_newline?(source)
|
|
593
614
|
source.end_with?("\n")
|
|
594
615
|
end
|
|
595
616
|
|
|
596
|
-
def merge_into_existing_class(source, decl, methods, state)
|
|
597
|
-
existing_pairs = collect_member_pairs(decl)
|
|
617
|
+
def merge_into_existing_class(source, decl, methods, state, existing_pairs)
|
|
598
618
|
new_methods, conflicting = partition_against_existing(methods, existing_pairs)
|
|
599
619
|
|
|
600
620
|
source = insert_into_class(source, decl, new_methods)
|
|
@@ -617,31 +637,21 @@ module Rigor
|
|
|
617
637
|
end
|
|
618
638
|
|
|
619
639
|
# Returns a list of `[method_name (Symbol), kind (Symbol)]` pairs for every method-like member in the
|
|
620
|
-
# declaration.
|
|
621
|
-
# three `attr_*` declaration kinds
|
|
622
|
-
# `attr_reader name: T` are treated as the same member
|
|
640
|
+
# declaration ({LayoutIndex.member_keys}); an `alias` declares its new name (#1975). ADR-14 slice 4
|
|
641
|
+
# recognises the three `attr_*` declaration kinds
|
|
642
|
+
# so a source-side `attr_reader :name` and an RBS-side `attr_reader name: T` are treated as the same member
|
|
643
|
+
# (i.e. user-authored); a `def self?.f` counts as both an instance and a singleton `f` (#1488).
|
|
623
644
|
def collect_member_pairs(decl)
|
|
624
|
-
|
|
625
|
-
decl.members.each { |m| collect_pairs_for_member(m, pairs) }
|
|
626
|
-
pairs
|
|
627
|
-
end
|
|
628
|
-
|
|
629
|
-
def collect_pairs_for_member(member, pairs)
|
|
630
|
-
case member
|
|
631
|
-
when RBS::AST::Members::MethodDefinition
|
|
632
|
-
pairs << [member.name, member.kind]
|
|
633
|
-
when RBS::AST::Members::AttrReader
|
|
634
|
-
pairs << [member.name, :instance]
|
|
635
|
-
when RBS::AST::Members::AttrWriter
|
|
636
|
-
pairs << [:"#{member.name}=", :instance]
|
|
637
|
-
when RBS::AST::Members::AttrAccessor
|
|
638
|
-
pairs << [member.name, :instance]
|
|
639
|
-
pairs << [:"#{member.name}=", :instance]
|
|
640
|
-
end
|
|
645
|
+
decl.members.flat_map { |m| LayoutIndex.member_keys(m) }
|
|
641
646
|
end
|
|
642
647
|
|
|
648
|
+
# A `def self?.f` candidate conflicts with a declared instance OR singleton `f`: inserting it beside a lone
|
|
649
|
+
# `def self.f` (or `def f`) would declare that half twice (#1488).
|
|
643
650
|
def partition_against_existing(methods, existing_pairs)
|
|
644
|
-
methods.partition
|
|
651
|
+
methods.partition do |c|
|
|
652
|
+
kinds = c.rbs_kind == :singleton_instance ? %i[instance singleton] : [c.kind]
|
|
653
|
+
kinds.none? { |kind| existing_pairs.include?([c.method_name, kind]) }
|
|
654
|
+
end
|
|
645
655
|
end
|
|
646
656
|
|
|
647
657
|
# Inserts each new method line one column before the class declaration's `end` keyword. The insertion
|
|
@@ -710,7 +720,7 @@ module Rigor
|
|
|
710
720
|
# part of the contract. A candidate that keeps `void` is still eligible, which is what case 2 is for —
|
|
711
721
|
# the `initialize` stub is always spelled `-> void`.
|
|
712
722
|
def replaces_declared_void?(candidate, decl)
|
|
713
|
-
member = find_method_member(decl, candidate
|
|
723
|
+
member = find_method_member(decl, candidate)
|
|
714
724
|
return false if member.nil?
|
|
715
725
|
|
|
716
726
|
declared_void = member.overloads.any? do |overload|
|
|
@@ -722,7 +732,7 @@ module Rigor
|
|
|
722
732
|
# Compares the existing member's source-side RBS text against the candidate's proposed RBS text
|
|
723
733
|
# ({.fewer_untyped?}).
|
|
724
734
|
def tightens_untyped?(candidate, decl, source)
|
|
725
|
-
member = find_method_member(decl, candidate
|
|
735
|
+
member = find_method_member(decl, candidate)
|
|
726
736
|
return false if member.nil?
|
|
727
737
|
|
|
728
738
|
self.class.fewer_untyped?(candidate.rbs, source[member.location.start_pos...member.location.end_pos])
|
|
@@ -737,7 +747,7 @@ module Rigor
|
|
|
737
747
|
# the same method (ADR-112 WD4), which it replaces in the inline attribute's own spelling; an
|
|
738
748
|
# `overloading?` member (`| ...`) is never its target.
|
|
739
749
|
def replaceable_member(decl, candidate)
|
|
740
|
-
member = find_method_member(decl, candidate
|
|
750
|
+
member = find_method_member(decl, candidate)
|
|
741
751
|
return member unless candidate.classification == Classification::INLINE_OVERWRITE
|
|
742
752
|
|
|
743
753
|
member = nil if member.respond_to?(:overloading?) && member.overloading?
|
|
@@ -746,15 +756,20 @@ module Rigor
|
|
|
746
756
|
|
|
747
757
|
def find_attr_member(decl, method_name, kind)
|
|
748
758
|
decl.members.find do |m|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
759
|
+
next false if m.is_a?(RBS::AST::Members::MethodDefinition) || m.is_a?(RBS::AST::Members::Alias)
|
|
760
|
+
|
|
761
|
+
LayoutIndex.member_keys(m).include?([method_name, kind])
|
|
752
762
|
end
|
|
753
763
|
end
|
|
754
764
|
|
|
755
|
-
|
|
765
|
+
# Matches on the candidate's {MethodCandidate#rbs_kind}, so a `def self?.f` proposal finds only a
|
|
766
|
+
# `def self?.f` member: a `def f` beside a `def self.f`, or a `def self?.f` under a plain `def f`
|
|
767
|
+
# proposal, is left as the user wrote it. An `overloading?` member (`def f: ... | ...`) adds to a
|
|
768
|
+
# declaration elsewhere rather than being one, so it is never replaced (#1488).
|
|
769
|
+
def find_method_member(decl, candidate)
|
|
756
770
|
decl.members.find do |m|
|
|
757
|
-
m.is_a?(RBS::AST::Members::MethodDefinition) && m.name == method_name &&
|
|
771
|
+
m.is_a?(RBS::AST::Members::MethodDefinition) && m.name == candidate.method_name &&
|
|
772
|
+
m.kind == candidate.rbs_kind && !m.overloading?
|
|
758
773
|
end
|
|
759
774
|
end
|
|
760
775
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "rooted_glob"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
# Classifies each configured `signature_paths:` entry by what it actually contributes to
|
|
5
7
|
# the RBS environment, so a caller can warn when a configured path resolves to nothing.
|
|
@@ -90,7 +92,7 @@ module Rigor
|
|
|
90
92
|
return Entry.new(path: path, status: :missing, rbs_file_count: 0) unless File.exist?(path)
|
|
91
93
|
return Entry.new(path: path, status: :not_directory, rbs_file_count: 0) unless File.directory?(path)
|
|
92
94
|
|
|
93
|
-
count =
|
|
95
|
+
count = RootedGlob.glob(path, "**/*.rbs").size
|
|
94
96
|
Entry.new(path: path, status: count.zero? ? :empty : :ok, rbs_file_count: count)
|
|
95
97
|
end
|
|
96
98
|
|
|
@@ -204,7 +206,7 @@ module Rigor
|
|
|
204
206
|
private_class_method :listed?
|
|
205
207
|
|
|
206
208
|
def self.canonical_rbs_files(dir)
|
|
207
|
-
|
|
209
|
+
RootedGlob.glob(dir, "**/*.rbs").filter_map { |file| canonical_path(file) }
|
|
208
210
|
end
|
|
209
211
|
private_class_method :canonical_rbs_files
|
|
210
212
|
|