rigortype 0.4.4 → 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/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- 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 +3 -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 +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- 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/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 +60 -15
- 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 +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- 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 +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- 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 +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- 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 +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -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 +45 -2
- 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 +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- 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 +5 -3
- 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-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -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-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 +13 -1
|
@@ -10,6 +10,7 @@ require_relative "../source/constant_path"
|
|
|
10
10
|
require_relative "../source/literals"
|
|
11
11
|
require_relative "../source/node_children"
|
|
12
12
|
require_relative "../inference/scope_indexer"
|
|
13
|
+
require_relative "../rooted_glob"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
15
16
|
module SigGen
|
|
@@ -32,6 +33,7 @@ module Rigor
|
|
|
32
33
|
# `let` / `subject` bindings ride on slice 5's optional `rigor-rspec` integration.
|
|
33
34
|
# - Calls whose receiver does not type as a `Type::Nominal` (e.g. `(some_dynamic).bar(...)`) are skipped —
|
|
34
35
|
# the collector cannot attribute them to a specific class.
|
|
36
|
+
# - A `T | nil` receiver with one `Nominal` / `Singleton` non-nil member counts as that member (#1389).
|
|
35
37
|
# - Zero-argument calls give no observation; methods are matched by `(class_name, method_name)` only.
|
|
36
38
|
class ObservationCollector # rubocop:disable Metrics/ClassLength
|
|
37
39
|
# @param paths — observe paths (files /
|
|
@@ -62,6 +64,12 @@ module Rigor
|
|
|
62
64
|
observations.transform_values(&:freeze).freeze
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
# Whether `arg` names something {#collect} reads: a directory, or a `.rb` file. The CLI's
|
|
68
|
+
# `observation` report splits roots on this, so "observed" means what the collector does (#1398).
|
|
69
|
+
def self.readable_root?(arg)
|
|
70
|
+
File.directory?(arg) || (File.file?(arg) && arg.end_with?(".rb"))
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
private
|
|
66
74
|
|
|
67
75
|
# Issue #821 — same universe as the generator and as `rigor check`; see
|
|
@@ -74,8 +82,8 @@ module Rigor
|
|
|
74
82
|
def resolve_paths(args)
|
|
75
83
|
args.flat_map do |arg|
|
|
76
84
|
if File.directory?(arg)
|
|
77
|
-
|
|
78
|
-
elsif
|
|
85
|
+
RootedGlob.glob(arg, "**/*.rb")
|
|
86
|
+
elsif self.class.readable_root?(arg)
|
|
79
87
|
[arg]
|
|
80
88
|
else
|
|
81
89
|
[]
|
|
@@ -92,8 +100,9 @@ module Rigor
|
|
|
92
100
|
base_scope = base_scope.with_discovery(base_scope.discovery.with(discovered_classes: discovered_classes))
|
|
93
101
|
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
94
102
|
bindings = collect_rspec_bindings(parse_result.value, scope_index)
|
|
103
|
+
@before_ivars = collect_before_ivars(parse_result.value, scope_index)
|
|
95
104
|
|
|
96
|
-
walk_calls(parse_result.value, scope_index, bindings, observations)
|
|
105
|
+
walk_calls(parse_result.value, scope_index, bindings, observations, [])
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
# Pre-walks `@source_paths` to collect every qualified class / module declaration. The result seeds the
|
|
@@ -145,22 +154,64 @@ module Rigor
|
|
|
145
154
|
end
|
|
146
155
|
end
|
|
147
156
|
|
|
148
|
-
|
|
157
|
+
# `blocks` is the chain of enclosing RSpec group / example / hook blocks, innermost last — the blocks a
|
|
158
|
+
# `before` ivar can reach the call from ({#before_ivar_type}). See {#enclosing_blocks} for where it is cut.
|
|
159
|
+
def walk_calls(node, scope_index, bindings, observations, blocks)
|
|
149
160
|
return unless node.is_a?(Prism::Node)
|
|
150
161
|
|
|
151
|
-
record_call(node, scope_index, bindings, observations) if node.is_a?(Prism::CallNode)
|
|
152
|
-
node.rigor_each_child
|
|
162
|
+
record_call(node, scope_index, bindings, observations, blocks) if node.is_a?(Prism::CallNode)
|
|
163
|
+
node.rigor_each_child do |child|
|
|
164
|
+
walk_calls(child, scope_index, bindings, observations, enclosing_blocks(node, child, blocks))
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Group, example and hook calls whose block runs with the example's ivars. `RSpec.` may prefix a group call.
|
|
169
|
+
RSPEC_GROUP_CALLS = %i[
|
|
170
|
+
describe context feature shared_examples shared_examples_for shared_context
|
|
171
|
+
].freeze
|
|
172
|
+
RSPEC_EXAMPLE_OR_HOOK_CALLS = %i[
|
|
173
|
+
it specify example scenario its before prepend_before append_before
|
|
174
|
+
].freeze
|
|
175
|
+
private_constant :RSPEC_GROUP_CALLS, :RSPEC_EXAMPLE_OR_HOOK_CALLS
|
|
176
|
+
|
|
177
|
+
# The chain `child` of `parent` sees. A group / example / hook call's block extends it; every other block or
|
|
178
|
+
# lambda (`Class.new { … }`, `instance_eval { … }`, `define_method(:m) { … }`, `each { … }`), a class / module /
|
|
179
|
+
# singleton-class body, and a `def` that is not a direct statement of a group block cut it, since their `self`
|
|
180
|
+
# may be another object.
|
|
181
|
+
def enclosing_blocks(parent, child, blocks)
|
|
182
|
+
case child
|
|
183
|
+
when Prism::BlockNode then rspec_scope_call?(parent) ? [*blocks, child] : []
|
|
184
|
+
when Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then []
|
|
185
|
+
when Prism::DefNode then group_statement?(child, blocks.last) ? blocks : []
|
|
186
|
+
else blocks
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def rspec_scope_call?(node)
|
|
191
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
192
|
+
return rspec_group_receiver?(node.receiver) if RSPEC_GROUP_CALLS.include?(node.name)
|
|
193
|
+
|
|
194
|
+
node.receiver.nil? && RSPEC_EXAMPLE_OR_HOOK_CALLS.include?(node.name)
|
|
153
195
|
end
|
|
154
196
|
|
|
155
|
-
def
|
|
197
|
+
def rspec_group_receiver?(receiver)
|
|
198
|
+
receiver.nil? || (receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :RSpec)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def group_statement?(node, block)
|
|
202
|
+
body = block&.body
|
|
203
|
+
body.is_a?(Prism::StatementsNode) && body.body.any? { |statement| statement.equal?(node) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def record_call(call_node, scope_index, bindings, observations, blocks)
|
|
156
207
|
receiver = call_node.receiver
|
|
157
208
|
return if receiver.nil?
|
|
158
209
|
|
|
159
210
|
scope = scope_index[call_node] || scope_index[receiver]
|
|
160
211
|
return if scope.nil?
|
|
161
212
|
|
|
162
|
-
receiver_type = resolve_receiver_type(receiver, scope, bindings)
|
|
163
|
-
key = observation_key(call_node, receiver_type)
|
|
213
|
+
receiver_type = resolve_receiver_type(receiver, scope, bindings, blocks)
|
|
214
|
+
key = observation_key(call_node, observed_receiver(receiver_type, call_node, scope))
|
|
164
215
|
return if key.nil?
|
|
165
216
|
|
|
166
217
|
observation = collect_args(call_node, scope)
|
|
@@ -181,6 +232,36 @@ module Rigor
|
|
|
181
232
|
end
|
|
182
233
|
end
|
|
183
234
|
|
|
235
|
+
# Issue #1389 — a receiver typed `T | nil` whose one non-nil member is a `Nominal` or `Singleton` is observed as
|
|
236
|
+
# that member. A Minitest `setup` ivar read in a test method types as `Foo?` (setup is not `initialize`), and a
|
|
237
|
+
# call of a method `nil` does not answer can only have reached `Foo#bar` with its arguments: on `nil` it raises
|
|
238
|
+
# `NoMethodError`, and `&.` skips the call. A method `NilClass`'s RBS declares (`==`, `respond_to?`, `is_a?`)
|
|
239
|
+
# may have run on `nil` instead, so a plain `.` call of one keeps the union unobserved. The sole member still
|
|
240
|
+
# has to pass {#observation_key}'s `Nominal` / `Singleton` checks, so `untyped | nil` stays unobserved, and a
|
|
241
|
+
# union with two non-nil members (`Foo | Bar | nil`) passes through unchanged. Observation only: the
|
|
242
|
+
# receiver's inferred type elsewhere is untouched.
|
|
243
|
+
def observed_receiver(receiver_type, call_node, scope)
|
|
244
|
+
return receiver_type unless receiver_type.is_a?(Type::Union)
|
|
245
|
+
|
|
246
|
+
non_nil = receiver_type.members.reject { |member| nil_type?(member) }
|
|
247
|
+
return receiver_type unless non_nil.size == 1
|
|
248
|
+
return receiver_type if !call_node.safe_navigation? && nil_answers?(call_node.name, scope)
|
|
249
|
+
|
|
250
|
+
non_nil.first
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Whether `nil` may answer `method_name`: `NilClass`'s RBS declares it, or that RBS is not available to ask.
|
|
254
|
+
def nil_answers?(method_name, scope)
|
|
255
|
+
loader = scope.environment.rbs_loader
|
|
256
|
+
return true if loader.nil? || loader.instance_definition("NilClass").nil?
|
|
257
|
+
|
|
258
|
+
!loader.instance_method(class_name: "NilClass", method_name: method_name).nil?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def nil_type?(type)
|
|
262
|
+
(type.is_a?(Type::Constant) && type.value.nil?) || (type.is_a?(Type::Nominal) && type.class_name == "NilClass")
|
|
263
|
+
end
|
|
264
|
+
|
|
184
265
|
# ADR-14 slice 5 — RSpec-aware receiver typing. Resolves a CallNode receiver against the collected
|
|
185
266
|
# `bindings` map (built by {#collect_rspec_bindings}) before falling back to ordinary `scope.type_of`. The
|
|
186
267
|
# three RSpec-shaped receivers we recognise:
|
|
@@ -190,11 +271,23 @@ module Rigor
|
|
|
190
271
|
# - `described_class.new(...)` chain — when the surrounding `describe Foo do … end` resolved `Foo`,
|
|
191
272
|
# return `Type::Nominal[Foo]`.
|
|
192
273
|
# - Anything else — pass through to `scope.type_of`, matching slice-3 behaviour.
|
|
193
|
-
def resolve_receiver_type(receiver, scope, bindings)
|
|
274
|
+
def resolve_receiver_type(receiver, scope, bindings, blocks)
|
|
194
275
|
return resolve_described_class_new(bindings) if described_class_new?(receiver)
|
|
195
276
|
return bindings[receiver.name] if bound_call?(receiver, bindings)
|
|
196
277
|
|
|
197
|
-
safe_type_of(scope, receiver)
|
|
278
|
+
type = safe_type_of(scope, receiver)
|
|
279
|
+
return type unless receiver.is_a?(Prism::InstanceVariableReadNode) && (type.nil? || type.is_a?(Type::Dynamic))
|
|
280
|
+
|
|
281
|
+
before_ivar_type(receiver.name, blocks) || type
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Issue #1389 — an ivar read inside an `it` block types as `Dynamic` (the block's ivars are not a class's), so
|
|
285
|
+
# a `before { @foo = Foo.new }` write stands in for it. Only a `before` whose example-group block encloses the
|
|
286
|
+
# read counts — a sibling `describe`, an un-included `shared_context` or a class body never reaches it — and
|
|
287
|
+
# when those `before`s write the name with more than one type, no type is answered.
|
|
288
|
+
def before_ivar_type(name, blocks)
|
|
289
|
+
types = blocks.flat_map { |block| @before_ivars.dig(block, name) || [] }.uniq
|
|
290
|
+
types.size == 1 ? types.first : nil
|
|
198
291
|
end
|
|
199
292
|
|
|
200
293
|
def bound_call?(receiver, bindings)
|
|
@@ -279,6 +372,39 @@ module Rigor
|
|
|
279
372
|
bindings[name] = body_type if body_type
|
|
280
373
|
end
|
|
281
374
|
|
|
375
|
+
# `before { @foo = Foo.new }` / `before(:each) do … end` inside an example-group block: records each ivar the
|
|
376
|
+
# `before` block's top-level statements write, with the written value's type, under that enclosing group
|
|
377
|
+
# block — `{ BlockNode => { :@foo => [Type] } }`, keyed by node identity. A `before` outside every group
|
|
378
|
+
# block ({#enclosing_blocks}), and `config.before`, record nothing.
|
|
379
|
+
def collect_before_ivars(root, scope_index)
|
|
380
|
+
index = {}.compare_by_identity
|
|
381
|
+
walk_before_ivars(root, [], index, scope_index)
|
|
382
|
+
index
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def walk_before_ivars(node, blocks, index, scope_index)
|
|
386
|
+
return unless node.is_a?(Prism::Node)
|
|
387
|
+
|
|
388
|
+
record_before_ivars(node, blocks.last, index, scope_index) if blocks.last
|
|
389
|
+
node.rigor_each_child do |child|
|
|
390
|
+
walk_before_ivars(child, enclosing_blocks(node, child, blocks), index, scope_index)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def record_before_ivars(node, group, index, scope_index)
|
|
395
|
+
return unless node.is_a?(Prism::CallNode) && node.name == :before && node.receiver.nil?
|
|
396
|
+
|
|
397
|
+
body = block_body_node(node.block)
|
|
398
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body].compact
|
|
399
|
+
statements.each do |statement|
|
|
400
|
+
next unless statement.is_a?(Prism::InstanceVariableWriteNode)
|
|
401
|
+
|
|
402
|
+
scope = scope_index[statement.value] || scope_index[statement]
|
|
403
|
+
type = scope && safe_type_of(scope, statement.value)
|
|
404
|
+
((index[group] ||= {})[statement.name] ||= []) << type if type
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
282
408
|
def binding_name_for(call_node)
|
|
283
409
|
first_arg = call_node.arguments&.arguments&.first
|
|
284
410
|
return call_node.name == :subject ? :subject : nil if first_arg.nil?
|
|
@@ -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
|