rigortype 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +23 -7
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +188 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +63 -16
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
- data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
- data/lib/rigor/analysis/runner.rb +212 -16
- data/lib/rigor/analysis/worker_session.rb +37 -3
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
- data/lib/rigor/cache/store.rb +17 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +47 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/failure_slot.rb +28 -0
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +743 -93
- data/lib/rigor/environment.rb +125 -25
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +61 -3
- data/lib/rigor/inference/hkt_sugar_translator.rb +87 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/plugin/registry.rb +25 -2
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +320 -85
- data/lib/rigor/sig_gen/renderer.rb +14 -12
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +9 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- metadata +71 -3
|
@@ -24,12 +24,16 @@ module Rigor
|
|
|
24
24
|
# body's last expression to derive an inferred return, looks up the project's existing RBS declaration (if
|
|
25
25
|
# any), and emits one {MethodCandidate} per def.
|
|
26
26
|
#
|
|
27
|
-
# The
|
|
28
|
-
# -
|
|
29
|
-
# defs are
|
|
30
|
-
# - Parameter
|
|
31
|
-
# `--params=observed`
|
|
32
|
-
#
|
|
27
|
+
# The scope, as it stands after the slices that widened the MVP:
|
|
28
|
+
# - Instance and singleton methods inside a nameable `class` / `module` body are considered. Top-level /
|
|
29
|
+
# DSL-block defs have no nameable receiver and are not candidates.
|
|
30
|
+
# - Parameter types are `untyped` per ADR-14 § "Robustness principle compliance" clause 2 unless
|
|
31
|
+
# `--params=observed` supplies call-site observations; the parameter LIST always mirrors the def's runtime
|
|
32
|
+
# shape (required / optional / rest / trailing / keyword / keyword-rest / forwarding / block). Issue #778
|
|
33
|
+
# retired the slice-1 gate that skipped every shape beyond required positionals as
|
|
34
|
+
# `sig.skipped.complex-shape`: the body typer binds every parameter to `untyped` when no RBS declares the
|
|
35
|
+
# method, so the inferred return is the same clause-1 answer for every shape, and the renderer the
|
|
36
|
+
# `initialize` stub already used spells them all.
|
|
33
37
|
# - A `Dynamic[top]` inferred return becomes `sig.skipped.untyped-return` — emitting `untyped` would obscure
|
|
34
38
|
# rather than help.
|
|
35
39
|
# - Tighter-return detection compares the RBS-erased spellings only when the existing declared return
|
|
@@ -41,7 +45,7 @@ module Rigor
|
|
|
41
45
|
UnrenderableMethod = Data.define(:path, :class_name, :method_name, :rbs, :error)
|
|
42
46
|
|
|
43
47
|
# @return [Array<UnrenderableMethod>] empty on a healthy run; read after {#run}.
|
|
44
|
-
attr_reader :unrenderable
|
|
48
|
+
attr_reader :unrenderable, :unresolvable_superclasses
|
|
45
49
|
|
|
46
50
|
# @param configuration [Rigor::Configuration]
|
|
47
51
|
# @param paths [Array<String>] files / directories to scan.
|
|
@@ -65,6 +69,10 @@ module Rigor
|
|
|
65
69
|
@meta_layouts = {}
|
|
66
70
|
# Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
|
|
67
71
|
@unrenderable = []
|
|
72
|
+
# Issue #735 — whole-run too: `{ class name => the superclass token that resolves nowhere }`.
|
|
73
|
+
@unresolvable_superclasses = {}
|
|
74
|
+
# Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
|
|
75
|
+
@superclass_nestings = {}
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
# Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
|
|
@@ -80,11 +88,229 @@ module Rigor
|
|
|
80
88
|
def run
|
|
81
89
|
@environment = build_environment
|
|
82
90
|
resolved = resolve_paths(@paths)
|
|
83
|
-
resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
91
|
+
candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
92
|
+
demote_overridden_base_methods(
|
|
93
|
+
demote_unresolvable_superclasses(resolve_superclass_spellings(candidates))
|
|
94
|
+
)
|
|
84
95
|
end
|
|
85
96
|
|
|
86
97
|
private
|
|
87
98
|
|
|
99
|
+
# Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
|
|
100
|
+
# override is not emitted itself.
|
|
101
|
+
#
|
|
102
|
+
# RBS resolves an undeclared subclass method through its ancestors, so a precise return written for
|
|
103
|
+
# the base becomes the subclass's answer. redmine's `FieldFormat::Base#target_class` honestly returns
|
|
104
|
+
# `nil`; `RecordList#target_class` overrides it with a real lookup that sig-gen could not type, and
|
|
105
|
+
# the emitted `def target_class: () -> nil` then produced four `undefined method … for nil` on the
|
|
106
|
+
# subclass's own working code, plus a `def.return-type-mismatch` telling the user their correct
|
|
107
|
+
# override was wrong.
|
|
108
|
+
#
|
|
109
|
+
# Emitting nothing puts both classes back on source inference, which answers correctly for each. The
|
|
110
|
+
# base keeps its signature whenever nothing overrides the method, and whenever the override IS
|
|
111
|
+
# emitted with a type of its own — the declaration is only dangerous when it is the only one.
|
|
112
|
+
#
|
|
113
|
+
# Whether an inherited declaration SHOULD outrank a class's own source `def` is a separate question
|
|
114
|
+
# (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
|
|
115
|
+
def demote_overridden_base_methods(candidates)
|
|
116
|
+
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
117
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup.sub(/\[.*\]\z/, "") }
|
|
118
|
+
end
|
|
119
|
+
return candidates if superclasses.empty?
|
|
120
|
+
|
|
121
|
+
overridden = unsigned_override_ancestors(candidates, superclasses)
|
|
122
|
+
return candidates if overridden.empty?
|
|
123
|
+
|
|
124
|
+
candidates.map do |candidate|
|
|
125
|
+
next candidate unless Classification::EMITTABLE.include?(candidate.classification)
|
|
126
|
+
next candidate unless overridden.include?([candidate.class_name, candidate.method_name])
|
|
127
|
+
|
|
128
|
+
demoted_candidate(candidate, :overridden_by_unsigned_subclass)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# `[class name, method name]` pairs an UNSIGNED override shadows: for every candidate that will not
|
|
133
|
+
# be emitted, every ancestor of its class paired with its method name.
|
|
134
|
+
def unsigned_override_ancestors(candidates, superclasses)
|
|
135
|
+
candidates.each_with_object(Set.new) do |candidate, acc|
|
|
136
|
+
next if Classification::EMITTABLE.include?(candidate.classification)
|
|
137
|
+
next if candidate.class_name.nil?
|
|
138
|
+
|
|
139
|
+
each_ancestor(candidate.class_name, superclasses) { |name| acc << [name, candidate.method_name] }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def each_ancestor(class_name, superclasses)
|
|
144
|
+
seen = {}
|
|
145
|
+
current = class_name
|
|
146
|
+
while (parent = superclasses[current]) && !seen[parent]
|
|
147
|
+
seen[parent] = true
|
|
148
|
+
yield parent
|
|
149
|
+
current = parent
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def demoted_candidate(candidate, reason)
|
|
154
|
+
MethodCandidate.new(
|
|
155
|
+
path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
|
|
156
|
+
kind: candidate.kind, classification: Classification::SKIPPED, skip_reason: reason,
|
|
157
|
+
inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
|
|
158
|
+
namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
|
|
159
|
+
class_superclasses: candidate.class_superclasses
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Issue #722 — resolves each recorded superclass token to the class Ruby means by it, and rewrites the
|
|
164
|
+
# emitted spelling when the two differ.
|
|
165
|
+
#
|
|
166
|
+
# `record_superclass` kept the source token verbatim on the reasoning that "RBS resolves it relative to
|
|
167
|
+
# the emitted namespace, matching Ruby's lexical scope". That holds only while the emitted namespace
|
|
168
|
+
# equals the source nesting, and sig-gen FLATTENS: `module Admin; class Record2 < Record` is written
|
|
169
|
+
# out as the compact `class Admin::Record2 < Record`, and RBS resolves a compact header's superclass at
|
|
170
|
+
# the TOP level (verified against the rbs gem), so the emitted token silently re-points at `::Record`
|
|
171
|
+
# where the program means `Admin::Record`. The checker resolves the same declaration correctly, so the
|
|
172
|
+
# generated sidecar contradicted the analyzer that wrote it.
|
|
173
|
+
#
|
|
174
|
+
# Candidate order is Ruby's own for the superclass expression: the header's nesting innermost-first,
|
|
175
|
+
# then the bare name — the reading `Scope#ancestor_name_candidates` owns for the analyzer's class
|
|
176
|
+
# graph. The question here is about the EMITTED tree, so the "is this a class" test is this run's
|
|
177
|
+
# declarations plus the RBS environment rather than the discovery tables; keep the two orders in step.
|
|
178
|
+
def resolve_superclass_spellings(candidates)
|
|
179
|
+
return candidates if @superclass_nestings.empty?
|
|
180
|
+
|
|
181
|
+
known = candidates.each_with_object(Set.new) do |candidate, acc|
|
|
182
|
+
acc << candidate.class_name if candidate.class_name
|
|
183
|
+
acc.merge(candidate.class_shells || [])
|
|
184
|
+
end
|
|
185
|
+
rewrites = superclass_rewrites(known)
|
|
186
|
+
return candidates if rewrites.empty?
|
|
187
|
+
|
|
188
|
+
candidates.map do |candidate|
|
|
189
|
+
overlap = rewrites.slice(*candidate.class_superclasses.keys)
|
|
190
|
+
next candidate if overlap.empty?
|
|
191
|
+
|
|
192
|
+
rebuild_with_superclasses(candidate, candidate.class_superclasses.merge(overlap))
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def superclass_rewrites(known)
|
|
197
|
+
@superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
|
|
198
|
+
next if token.nil? || nesting.empty?
|
|
199
|
+
|
|
200
|
+
resolved = resolve_superclass_token(token, nesting, known)
|
|
201
|
+
acc[full] = resolved if resolved && resolved != token
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The first candidate spelling that names a class this run declares or the RBS environment knows, or
|
|
206
|
+
# nil when none does — an unresolvable superclass is #735's business, not this pass's.
|
|
207
|
+
def resolve_superclass_token(token, nesting, known)
|
|
208
|
+
base = token.sub(/\[.*\]\z/, "")
|
|
209
|
+
args = token[base.length..] || ""
|
|
210
|
+
nesting.length.downto(1) do |i|
|
|
211
|
+
candidate = "#{nesting[0, i].join('::')}::#{base}"
|
|
212
|
+
return "#{candidate}#{args}" if known.include?(candidate) ||
|
|
213
|
+
Reflection.rbs_class_known?(candidate, environment: @environment)
|
|
214
|
+
end
|
|
215
|
+
token
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def rebuild_with_superclasses(candidate, superclasses)
|
|
219
|
+
MethodCandidate.new(
|
|
220
|
+
path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
|
|
221
|
+
kind: candidate.kind, classification: candidate.classification,
|
|
222
|
+
inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
|
|
223
|
+
rbs: candidate.rbs, skip_reason: candidate.skip_reason,
|
|
224
|
+
namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
|
|
225
|
+
class_superclasses: superclasses
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Issue #735 — a generated declaration whose superclass chain does not terminate in a class the RBS
|
|
230
|
+
# environment knows is DROPPED rather than emitted.
|
|
231
|
+
#
|
|
232
|
+
# `record_superclass` is right that a subclass declaration must carry its superclass: without it the
|
|
233
|
+
# sidecar misrepresents the class and dispatch degrades. But emitting one the environment cannot
|
|
234
|
+
# resolve is worse than either — `RBS::NoSuperclassFoundError` collapses the class, so every call into
|
|
235
|
+
# it reads `Dynamic[top]` and the whole declaration is a liability. On redmine, `class Principal <
|
|
236
|
+
# ApplicationRecord` (nothing declares `ApplicationRecord`, and nothing could: the chain ends at
|
|
237
|
+
# `ActiveRecord::Base`, which no RBS in the environment carries) collapsed 98 classes on the very next
|
|
238
|
+
# run. A chain that LOOPS is dropped for the same reason, and is the shape that made an
|
|
239
|
+
# `AncestorBuilder` walk recurse until `SystemStackError` (#609).
|
|
240
|
+
#
|
|
241
|
+
# Dropping is the conservative end: no declaration leaves the class exactly as it was before sig-gen
|
|
242
|
+
# ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
|
|
243
|
+
# OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
|
|
244
|
+
# every inherited call fires `call.undefined-method`.
|
|
245
|
+
def demote_unresolvable_superclasses(candidates)
|
|
246
|
+
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
247
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
|
|
248
|
+
end
|
|
249
|
+
# Everything this run declares, superclass or not: a chain that reaches `class Record` — emitted
|
|
250
|
+
# here, with no superclass of its own — terminates in a class the next run WILL know.
|
|
251
|
+
emitted = candidates.each_with_object(Set.new) do |candidate, acc|
|
|
252
|
+
acc << candidate.class_name if candidate.class_name
|
|
253
|
+
acc.merge(candidate.class_shells || [])
|
|
254
|
+
end
|
|
255
|
+
unresolvable = superclasses.each_with_object({}) do |(name, _sup), acc|
|
|
256
|
+
root = unresolved_superclass_root(name, superclasses, emitted)
|
|
257
|
+
acc[name] = root if root
|
|
258
|
+
end
|
|
259
|
+
return candidates if unresolvable.empty?
|
|
260
|
+
|
|
261
|
+
candidates.map do |candidate|
|
|
262
|
+
owner = unresolvable_for(candidate.class_name, unresolvable)
|
|
263
|
+
next candidate if owner.nil?
|
|
264
|
+
|
|
265
|
+
record_unresolvable_superclass(candidate, owner)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
|
|
270
|
+
# INSIDE `class WikiPage < ApplicationRecord`, so keeping the inner one while dropping the outer emits
|
|
271
|
+
# the unresolvable header anyway — the wrapper is not a candidate of its own and carries no methods,
|
|
272
|
+
# so nothing else would have demoted it.
|
|
273
|
+
def unresolvable_for(class_name, unresolvable)
|
|
274
|
+
return nil if class_name.nil?
|
|
275
|
+
return [class_name, unresolvable[class_name]] if unresolvable.key?(class_name)
|
|
276
|
+
|
|
277
|
+
unresolvable.each do |name, root|
|
|
278
|
+
return [name, root] if class_name.start_with?("#{name}::")
|
|
279
|
+
end
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# The first superclass token on `name`'s recorded chain that neither the RBS environment knows nor this
|
|
284
|
+
# run emits, or nil when the chain terminates in a known class. A chain that revisits a class returns
|
|
285
|
+
# that class's own superclass token — a cycle resolves nowhere.
|
|
286
|
+
def unresolved_superclass_root(name, superclasses, emitted, seen = {})
|
|
287
|
+
superclass = superclasses[name]
|
|
288
|
+
return nil if superclass.nil?
|
|
289
|
+
return superclass if seen[name]
|
|
290
|
+
|
|
291
|
+
seen[name] = true
|
|
292
|
+
# The recorded spelling may carry the type arguments {#generic_superclass_spelling} added; the
|
|
293
|
+
# resolvability question is about the class, so it is asked of the bare name.
|
|
294
|
+
base = superclass.sub(/\[.*\]\z/, "")
|
|
295
|
+
return nil if Reflection.rbs_class_known?(base, environment: @environment)
|
|
296
|
+
return superclass unless emitted.include?(base)
|
|
297
|
+
|
|
298
|
+
unresolved_superclass_root(base, superclasses, emitted, seen)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def record_unresolvable_superclass(candidate, owner)
|
|
302
|
+
class_name, root = owner
|
|
303
|
+
@unresolvable_superclasses[class_name] ||= root
|
|
304
|
+
MethodCandidate.new(
|
|
305
|
+
path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
|
|
306
|
+
kind: candidate.kind, classification: Classification::SKIPPED,
|
|
307
|
+
skip_reason: :unresolvable_superclass, inferred_return: candidate.inferred_return,
|
|
308
|
+
declared_return_rbs: candidate.declared_return_rbs,
|
|
309
|
+
namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
|
|
310
|
+
class_superclasses: candidate.class_superclasses
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
|
|
88
314
|
def build_environment
|
|
89
315
|
Environment.for_project(
|
|
90
316
|
libraries: @configuration.libraries,
|
|
@@ -193,7 +419,7 @@ module Rigor
|
|
|
193
419
|
|
|
194
420
|
full = (prefix + [name]).join("::")
|
|
195
421
|
@namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
|
|
196
|
-
record_superclass(node, full)
|
|
422
|
+
record_superclass(node, full, prefix)
|
|
197
423
|
walk_namespace_body(node, prefix + [name], out)
|
|
198
424
|
true
|
|
199
425
|
end
|
|
@@ -207,11 +433,38 @@ module Rigor
|
|
|
207
433
|
# superclass (`Class.new`, any other `CallNode`) is left unrecorded — a `Data.define` / `Struct.new` one is
|
|
208
434
|
# already carried by {#register_meta_classes}, and the rest are un-representable, where guessing would
|
|
209
435
|
# misfold.
|
|
210
|
-
def record_superclass(node, full)
|
|
436
|
+
def record_superclass(node, full, prefix)
|
|
211
437
|
return unless node.is_a?(Prism::ClassNode)
|
|
212
438
|
|
|
213
439
|
superclass = qualified_constant_path(node.superclass)
|
|
214
|
-
|
|
440
|
+
return if superclass.nil?
|
|
441
|
+
|
|
442
|
+
# Issue #722 — the nesting the header is WRITTEN in, kept whole-run (not per-file scratch) because
|
|
443
|
+
# the name it resolves to may be a class another file declares. Resolved once every file has been
|
|
444
|
+
# walked, in {#resolve_superclass_spellings}.
|
|
445
|
+
spelling = generic_superclass_spelling(superclass)
|
|
446
|
+
# The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
|
|
447
|
+
# file, while this table is whole-run.
|
|
448
|
+
@superclass_nestings[full] = [prefix.dup, spelling]
|
|
449
|
+
@class_superclasses[full] = spelling
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
|
|
453
|
+
# yields `RBS::InvalidTypeApplicationError: ::Array expects parameters [unchecked out E], but given
|
|
454
|
+
# args []`, which fails the build for that class exactly as an unresolvable superclass does — redmine
|
|
455
|
+
# subclasses `Array` five times. The element type is not something this pass can infer, so it is
|
|
456
|
+
# `untyped`: the subclass's OWN methods are what sig-gen is here to record, and `Array[untyped]` keeps
|
|
457
|
+
# the inherited surface resolving instead of collapsing the class.
|
|
458
|
+
def generic_superclass_spelling(superclass)
|
|
459
|
+
loader = @environment&.rbs_loader
|
|
460
|
+
return superclass if loader.nil?
|
|
461
|
+
|
|
462
|
+
arity = loader.class_type_param_names(superclass).size
|
|
463
|
+
return superclass if arity.zero?
|
|
464
|
+
|
|
465
|
+
"#{superclass}[#{(['untyped'] * arity).join(', ')}]"
|
|
466
|
+
rescue StandardError
|
|
467
|
+
superclass
|
|
215
468
|
end
|
|
216
469
|
|
|
217
470
|
# The ADR-48 member layouts for this file, in one table keyed by qualified class name. Reading the engine's
|
|
@@ -401,8 +654,8 @@ module Rigor
|
|
|
401
654
|
end
|
|
402
655
|
|
|
403
656
|
# Emits `def initialize: (<shape>) -> void`. The return is always `void` because Ruby's `initialize`
|
|
404
|
-
# return value is never meaningful. The parameter list mirrors the runtime shape
|
|
405
|
-
#
|
|
657
|
+
# return value is never meaningful. The parameter list mirrors the runtime shape through the same
|
|
658
|
+
# {#render_param_list} every other def renders with.
|
|
406
659
|
#
|
|
407
660
|
# When `--params=observed` populates `@observations` for `[class_name, :initialize]` (via the
|
|
408
661
|
# `ObservationCollector`'s `.new` → `:initialize` routing), positional and keyword arg types come from the
|
|
@@ -410,7 +663,7 @@ module Rigor
|
|
|
410
663
|
# clause 2.
|
|
411
664
|
def initialize_stub_candidate(path, def_node, class_name)
|
|
412
665
|
params = def_node.parameters
|
|
413
|
-
rbs = "def initialize: (#{
|
|
666
|
+
rbs = "def initialize: (#{render_param_list(params, class_name, :initialize)})" \
|
|
414
667
|
"#{block_signature_suffix(params)} -> void"
|
|
415
668
|
build_candidate(
|
|
416
669
|
path: path, class_name: class_name, method_name: :initialize,
|
|
@@ -419,57 +672,81 @@ module Rigor
|
|
|
419
672
|
)
|
|
420
673
|
end
|
|
421
674
|
|
|
422
|
-
def
|
|
675
|
+
# The RBS parameter list of a def, mirroring its runtime shape position by position: `untyped` per
|
|
676
|
+
# required positional, `?untyped` per optional, `*untyped` for a rest, one `untyped` per trailing
|
|
677
|
+
# positional after the rest, `name: untyped` / `?name: untyped` per keyword, `**untyped` for a keyword
|
|
678
|
+
# rest, and `*untyped, **untyped` for `...` forwarding (its block half is {#block_signature_suffix}'s).
|
|
679
|
+
# `**nil` declares that no keywords are accepted, which RBS cannot spell, so it renders nothing.
|
|
680
|
+
#
|
|
681
|
+
# Per ADR-5 clause 2 every position is `untyped` unless `--params=observed` supplied call-site
|
|
682
|
+
# observations, in which case each leading positional and each keyword carries the union of the types the
|
|
683
|
+
# arity-compatible observations passed there ({#arity_matched_observations}). The rest, the trailing
|
|
684
|
+
# positionals and the keyword rest stay `untyped`: an observation does not say which of its arguments the
|
|
685
|
+
# splat absorbed. Before #778 only `initialize` rendered through this path and every other def with a
|
|
686
|
+
# shape beyond required positionals was skipped as `sig.skipped.complex-shape`.
|
|
687
|
+
def render_param_list(params, class_name, method_name)
|
|
423
688
|
return "" unless params.is_a?(Prism::ParametersNode)
|
|
424
689
|
|
|
425
|
-
observations =
|
|
426
|
-
offset = 0
|
|
690
|
+
observations = arity_matched_observations(class_name, method_name, params)
|
|
427
691
|
parts = []
|
|
428
|
-
|
|
429
|
-
params.requireds.
|
|
430
|
-
|
|
431
|
-
end
|
|
432
|
-
offset += params.requireds.size
|
|
433
|
-
|
|
434
|
-
params.optionals.each_with_index do |_, i|
|
|
435
|
-
parts << initialize_positional_type(observations, offset + i, "?")
|
|
436
|
-
end
|
|
437
|
-
|
|
692
|
+
params.requireds.each_index { |i| parts << positional_type(observations, i, "") }
|
|
693
|
+
offset = params.requireds.size
|
|
694
|
+
params.optionals.each_index { |i| parts << positional_type(observations, offset + i, "?") }
|
|
438
695
|
parts << "*untyped" if params.rest
|
|
696
|
+
parts.concat(Array.new(params.posts.size, "untyped"))
|
|
439
697
|
params.keywords.each { |kw| parts << render_keyword_param(kw, observations) }
|
|
440
|
-
parts
|
|
698
|
+
parts.concat(keyword_rest_parts(params))
|
|
441
699
|
parts.join(", ")
|
|
442
700
|
end
|
|
443
701
|
|
|
702
|
+
def keyword_rest_parts(params)
|
|
703
|
+
case params.keyword_rest
|
|
704
|
+
when Prism::ForwardingParameterNode
|
|
705
|
+
# `...` forwards positionals as well as keywords; a rest already rendered covers the former.
|
|
706
|
+
params.rest ? ["**untyped"] : ["*untyped", "**untyped"]
|
|
707
|
+
when Prism::KeywordRestParameterNode then ["**untyped"]
|
|
708
|
+
else []
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
444
712
|
# The RBS block suffix for a `def` that takes a `&block` parameter, e.g. ` ?{ (*untyped) -> untyped }`.
|
|
445
713
|
# A block belongs AFTER the parameter parens in RBS (`(params) ?{ block } -> ret`), not inside them —
|
|
446
714
|
# emitting it as a comma-joined member produced `(**untyped, ?{ (?) -> void })`, which RBS rejects
|
|
447
715
|
# (`optional keyword argument type is expected`) and which then collapsed the whole env build. sig-gen
|
|
448
716
|
# never observes the block's own signature, so it is rendered maximally lenient (ADR-5): an optional
|
|
449
|
-
# block (`?{`, since a `&block` need not be passed) taking `*untyped` and returning `untyped`.
|
|
450
|
-
# "" when the method takes no block.
|
|
717
|
+
# block (`?{`, since a `&block` need not be passed) taking `*untyped` and returning `untyped`. `...`
|
|
718
|
+
# forwards a block too and gets the same suffix. Returns "" when the method takes no block.
|
|
451
719
|
def block_signature_suffix(params)
|
|
452
720
|
return "" unless params.is_a?(Prism::ParametersNode)
|
|
453
|
-
return ""
|
|
721
|
+
return "" unless params.block || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
454
722
|
|
|
455
723
|
" ?{ (*untyped) -> untyped }"
|
|
456
724
|
end
|
|
457
725
|
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
# overload the
|
|
461
|
-
|
|
726
|
+
# Observations under `[class_name, method_name]` whose positional arity the def accepts: at least its
|
|
727
|
+
# required count (leading plus trailing), and, unless a rest or `...` absorbs the surplus, at most
|
|
728
|
+
# required plus optional. An arity outside that window describes a different overload the signature
|
|
729
|
+
# cannot express, so it says nothing about these positions. #778 widened the window for ordinary defs
|
|
730
|
+
# from the exact required count — `def optional(text = "x")` called as `optional` AND as `optional("y")`
|
|
731
|
+
# now credits both call sites.
|
|
732
|
+
def arity_matched_observations(class_name, method_name, params)
|
|
462
733
|
return [] if @observations.empty?
|
|
463
734
|
|
|
464
|
-
list = @observations[[class_name,
|
|
465
|
-
min = params.requireds.size
|
|
466
|
-
max = min + params.optionals.size
|
|
467
|
-
list.select { |obs|
|
|
735
|
+
list = @observations[[class_name, method_name]] || []
|
|
736
|
+
min = params.requireds.size + params.posts.size
|
|
737
|
+
max = unbounded_positionals?(params) ? Float::INFINITY : min + params.optionals.size
|
|
738
|
+
list.select { |obs| obs.positional.size.between?(min, max) }
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def unbounded_positionals?(params)
|
|
742
|
+
!params.rest.nil? || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
468
743
|
end
|
|
469
744
|
|
|
470
|
-
|
|
745
|
+
# A bare union is a valid positional type in RBS (`(String | Integer, ?String | Integer)`), so unlike the
|
|
746
|
+
# return position it is not parenthesised.
|
|
747
|
+
def positional_type(observations, index, prefix)
|
|
471
748
|
types = observations.filter_map { |obs| obs.positional[index] }
|
|
472
|
-
"#{prefix}#{
|
|
749
|
+
"#{prefix}#{union_erase(types)}"
|
|
473
750
|
end
|
|
474
751
|
|
|
475
752
|
def render_keyword_param(keyword, observations)
|
|
@@ -497,10 +774,6 @@ module Rigor
|
|
|
497
774
|
return nil if initialize_excludes?(def_node, kind)
|
|
498
775
|
return initialize_stub_candidate(path, def_node, class_name) if non_trivial_initialize?(def_node, kind)
|
|
499
776
|
|
|
500
|
-
unless simple_parameter_shape?(def_node.parameters)
|
|
501
|
-
return skipped(path, def_node, class_name, kind, :complex_shape)
|
|
502
|
-
end
|
|
503
|
-
|
|
504
777
|
inferred = infer_return_type(def_node, scope_index)
|
|
505
778
|
return skipped(path, def_node, class_name, kind, :untyped_return) if inferred.nil? || dynamic_top?(inferred)
|
|
506
779
|
|
|
@@ -514,20 +787,6 @@ module Rigor
|
|
|
514
787
|
end
|
|
515
788
|
end
|
|
516
789
|
|
|
517
|
-
# Required positionals only; the MVP's body-typing path gives well-defined returns for that shape.
|
|
518
|
-
# Optional / rest / keyword / block parameters route through the `sig.skipped.complex-shape` reason until
|
|
519
|
-
# slices 3+ widen the param policy.
|
|
520
|
-
def simple_parameter_shape?(params)
|
|
521
|
-
return true if params.nil?
|
|
522
|
-
return false unless params.is_a?(Prism::ParametersNode)
|
|
523
|
-
|
|
524
|
-
params.optionals.empty? &&
|
|
525
|
-
params.rest.nil? &&
|
|
526
|
-
params.keywords.empty? &&
|
|
527
|
-
params.keyword_rest.nil? &&
|
|
528
|
-
params.block.nil?
|
|
529
|
-
end
|
|
530
|
-
|
|
531
790
|
# Mirrors the `def.return-type-mismatch` rule's body-type extraction: type the implicit-return expression
|
|
532
791
|
# under the scope the indexer associated with the body. The parameter bindings (typed `untyped` per the
|
|
533
792
|
# indexer's default) come from `with_local` inside `StatementEvaluator`; the result is the carrier the
|
|
@@ -742,8 +1001,8 @@ module Rigor
|
|
|
742
1001
|
end
|
|
743
1002
|
|
|
744
1003
|
def render_rbs_line(def_node, inferred, class_name, kind)
|
|
745
|
-
|
|
746
|
-
head =
|
|
1004
|
+
params = def_node.parameters
|
|
1005
|
+
head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
|
|
747
1006
|
prefix = method_def_prefix(class_name, def_node.name, kind)
|
|
748
1007
|
"#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred))}"
|
|
749
1008
|
end
|
|
@@ -779,30 +1038,6 @@ module Rigor
|
|
|
779
1038
|
false
|
|
780
1039
|
end
|
|
781
1040
|
|
|
782
|
-
def required_arity(def_node)
|
|
783
|
-
params = def_node.parameters
|
|
784
|
-
params.is_a?(Prism::ParametersNode) ? params.requireds.size : 0
|
|
785
|
-
end
|
|
786
|
-
|
|
787
|
-
# Per ADR-5 clause 2 the default is `untyped` for every position. Observed-policy callers
|
|
788
|
-
# (`--params=observed`) pass an `observations:` map at construction time; the generator unions
|
|
789
|
-
# per-position arg types whose tuple arity matches the def's required-positional count. Observations from
|
|
790
|
-
# arities other than the def's count are discarded — they describe a different overload the MVP does not
|
|
791
|
-
# emit.
|
|
792
|
-
def render_param_list(class_name, method_name, arity)
|
|
793
|
-
tuples = matching_observations(class_name, method_name, arity)
|
|
794
|
-
return Array.new(arity, "untyped").join(", ") if tuples.empty?
|
|
795
|
-
|
|
796
|
-
Array.new(arity) { |i| union_erase(tuples.map { |obs| obs.positional[i] }) }.join(", ")
|
|
797
|
-
end
|
|
798
|
-
|
|
799
|
-
def matching_observations(class_name, method_name, arity)
|
|
800
|
-
return [] if @observations.empty?
|
|
801
|
-
|
|
802
|
-
list = @observations[[class_name, method_name]] || []
|
|
803
|
-
list.select { |obs| obs.positional.size == arity }
|
|
804
|
-
end
|
|
805
|
-
|
|
806
1041
|
def union_erase(types)
|
|
807
1042
|
return "untyped" if types.empty?
|
|
808
1043
|
return elaborated_rbs(types.first) if types.size == 1
|
|
@@ -14,7 +14,8 @@ module Rigor
|
|
|
14
14
|
# - `:diff` — a unified-style diff comparing the existing RBS spelling (if any) against the inferred
|
|
15
15
|
# spelling. The MVP renders a minimal "- declared / + inferred" block; full per-file diffing arrives with
|
|
16
16
|
# slice 2's `--write` merge.
|
|
17
|
-
# - `:json` — machine-readable payload with the same classification table as `:print
|
|
17
|
+
# - `:json` — machine-readable payload with the same classification table as `:print`, plus every `skipped`
|
|
18
|
+
# row with its `skip_reason` (#778).
|
|
18
19
|
class Renderer
|
|
19
20
|
def initialize(out:)
|
|
20
21
|
@out = out
|
|
@@ -27,11 +28,10 @@ module Rigor
|
|
|
27
28
|
# {Classification} constants to include; an empty
|
|
28
29
|
# array means "all emittable classifications".
|
|
29
30
|
def render(candidates:, mode:, format:, selection:)
|
|
30
|
-
filtered = filter(candidates, selection)
|
|
31
|
-
|
|
32
31
|
case format
|
|
33
|
-
when "json" then render_json(
|
|
32
|
+
when "json" then render_json(filter(candidates, selection, with_skipped: true))
|
|
34
33
|
when "text"
|
|
34
|
+
filtered = filter(candidates, selection)
|
|
35
35
|
mode == :diff ? render_diff(filtered) : render_print(filtered)
|
|
36
36
|
else
|
|
37
37
|
raise ArgumentError, "unsupported format: #{format}"
|
|
@@ -40,14 +40,16 @@ module Rigor
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def filter(candidates, selection)
|
|
49
|
-
active = selection.empty? ? EMITTABLE : selection
|
|
50
|
-
candidates.select
|
|
43
|
+
# The emittable rows the selection asks for. JSON also carries every `skipped` row whatever the selection:
|
|
44
|
+
# ADR-14 makes the JSON payload the surface where `sig.skipped.*` is reported, and a consumer asking why a
|
|
45
|
+
# method is missing from its `sig/` needs the reason next to the rows that did emit (#778 — the rows were
|
|
46
|
+
# built with their `skip_reason` and then dropped here). `equivalent` rows stay out: nothing to do,
|
|
47
|
+
# nothing to explain.
|
|
48
|
+
def filter(candidates, selection, with_skipped: false)
|
|
49
|
+
active = selection.empty? ? Classification::EMITTABLE : selection
|
|
50
|
+
candidates.select do |c|
|
|
51
|
+
active.include?(c.classification) || (with_skipped && c.classification == Classification::SKIPPED)
|
|
52
|
+
end
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def render_print(candidates)
|
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -90,6 +90,9 @@ module Rigor
|
|
|
90
90
|
target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
|
|
94
|
+
# `:new_file` row (the generator does not classify one), and admitting it here would let a
|
|
95
|
+
# classification with no writer behind it decide that a target file must be created.
|
|
93
96
|
EMITTABLE = [Classification::NEW_METHOD, Classification::TIGHTER_RETURN].freeze
|
|
94
97
|
private_constant :EMITTABLE
|
|
95
98
|
|