rigortype 0.3.6 → 0.3.7
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/manual/02-cli-reference.md +16 -7
- 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 +116 -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 +44 -9
- 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 +93 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +147 -10
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- 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 +31 -8
- data/lib/rigor/cache/store.rb +10 -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 +18 -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/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 +525 -46
- data/lib/rigor/environment.rb +38 -15
- 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 +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -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/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/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 +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- 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 +3 -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 +69 -3
|
@@ -41,7 +41,7 @@ module Rigor
|
|
|
41
41
|
UnrenderableMethod = Data.define(:path, :class_name, :method_name, :rbs, :error)
|
|
42
42
|
|
|
43
43
|
# @return [Array<UnrenderableMethod>] empty on a healthy run; read after {#run}.
|
|
44
|
-
attr_reader :unrenderable
|
|
44
|
+
attr_reader :unrenderable, :unresolvable_superclasses
|
|
45
45
|
|
|
46
46
|
# @param configuration [Rigor::Configuration]
|
|
47
47
|
# @param paths [Array<String>] files / directories to scan.
|
|
@@ -65,6 +65,10 @@ module Rigor
|
|
|
65
65
|
@meta_layouts = {}
|
|
66
66
|
# Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
|
|
67
67
|
@unrenderable = []
|
|
68
|
+
# Issue #735 — whole-run too: `{ class name => the superclass token that resolves nowhere }`.
|
|
69
|
+
@unresolvable_superclasses = {}
|
|
70
|
+
# Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
|
|
71
|
+
@superclass_nestings = {}
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
# Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
|
|
@@ -80,11 +84,229 @@ module Rigor
|
|
|
80
84
|
def run
|
|
81
85
|
@environment = build_environment
|
|
82
86
|
resolved = resolve_paths(@paths)
|
|
83
|
-
resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
87
|
+
candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
88
|
+
demote_overridden_base_methods(
|
|
89
|
+
demote_unresolvable_superclasses(resolve_superclass_spellings(candidates))
|
|
90
|
+
)
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
private
|
|
87
94
|
|
|
95
|
+
# Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
|
|
96
|
+
# override is not emitted itself.
|
|
97
|
+
#
|
|
98
|
+
# RBS resolves an undeclared subclass method through its ancestors, so a precise return written for
|
|
99
|
+
# the base becomes the subclass's answer. redmine's `FieldFormat::Base#target_class` honestly returns
|
|
100
|
+
# `nil`; `RecordList#target_class` overrides it with a real lookup that sig-gen could not type, and
|
|
101
|
+
# the emitted `def target_class: () -> nil` then produced four `undefined method … for nil` on the
|
|
102
|
+
# subclass's own working code, plus a `def.return-type-mismatch` telling the user their correct
|
|
103
|
+
# override was wrong.
|
|
104
|
+
#
|
|
105
|
+
# Emitting nothing puts both classes back on source inference, which answers correctly for each. The
|
|
106
|
+
# base keeps its signature whenever nothing overrides the method, and whenever the override IS
|
|
107
|
+
# emitted with a type of its own — the declaration is only dangerous when it is the only one.
|
|
108
|
+
#
|
|
109
|
+
# Whether an inherited declaration SHOULD outrank a class's own source `def` is a separate question
|
|
110
|
+
# (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
|
|
111
|
+
def demote_overridden_base_methods(candidates)
|
|
112
|
+
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
113
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup.sub(/\[.*\]\z/, "") }
|
|
114
|
+
end
|
|
115
|
+
return candidates if superclasses.empty?
|
|
116
|
+
|
|
117
|
+
overridden = unsigned_override_ancestors(candidates, superclasses)
|
|
118
|
+
return candidates if overridden.empty?
|
|
119
|
+
|
|
120
|
+
candidates.map do |candidate|
|
|
121
|
+
next candidate unless Classification::EMITTABLE.include?(candidate.classification)
|
|
122
|
+
next candidate unless overridden.include?([candidate.class_name, candidate.method_name])
|
|
123
|
+
|
|
124
|
+
demoted_candidate(candidate, :overridden_by_unsigned_subclass)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# `[class name, method name]` pairs an UNSIGNED override shadows: for every candidate that will not
|
|
129
|
+
# be emitted, every ancestor of its class paired with its method name.
|
|
130
|
+
def unsigned_override_ancestors(candidates, superclasses)
|
|
131
|
+
candidates.each_with_object(Set.new) do |candidate, acc|
|
|
132
|
+
next if Classification::EMITTABLE.include?(candidate.classification)
|
|
133
|
+
next if candidate.class_name.nil?
|
|
134
|
+
|
|
135
|
+
each_ancestor(candidate.class_name, superclasses) { |name| acc << [name, candidate.method_name] }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def each_ancestor(class_name, superclasses)
|
|
140
|
+
seen = {}
|
|
141
|
+
current = class_name
|
|
142
|
+
while (parent = superclasses[current]) && !seen[parent]
|
|
143
|
+
seen[parent] = true
|
|
144
|
+
yield parent
|
|
145
|
+
current = parent
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def demoted_candidate(candidate, reason)
|
|
150
|
+
MethodCandidate.new(
|
|
151
|
+
path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
|
|
152
|
+
kind: candidate.kind, classification: Classification::SKIPPED, skip_reason: reason,
|
|
153
|
+
inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
|
|
154
|
+
namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
|
|
155
|
+
class_superclasses: candidate.class_superclasses
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Issue #722 — resolves each recorded superclass token to the class Ruby means by it, and rewrites the
|
|
160
|
+
# emitted spelling when the two differ.
|
|
161
|
+
#
|
|
162
|
+
# `record_superclass` kept the source token verbatim on the reasoning that "RBS resolves it relative to
|
|
163
|
+
# the emitted namespace, matching Ruby's lexical scope". That holds only while the emitted namespace
|
|
164
|
+
# equals the source nesting, and sig-gen FLATTENS: `module Admin; class Record2 < Record` is written
|
|
165
|
+
# out as the compact `class Admin::Record2 < Record`, and RBS resolves a compact header's superclass at
|
|
166
|
+
# the TOP level (verified against the rbs gem), so the emitted token silently re-points at `::Record`
|
|
167
|
+
# where the program means `Admin::Record`. The checker resolves the same declaration correctly, so the
|
|
168
|
+
# generated sidecar contradicted the analyzer that wrote it.
|
|
169
|
+
#
|
|
170
|
+
# Candidate order is Ruby's own for the superclass expression: the header's nesting innermost-first,
|
|
171
|
+
# then the bare name — the reading `Scope#ancestor_name_candidates` owns for the analyzer's class
|
|
172
|
+
# graph. The question here is about the EMITTED tree, so the "is this a class" test is this run's
|
|
173
|
+
# declarations plus the RBS environment rather than the discovery tables; keep the two orders in step.
|
|
174
|
+
def resolve_superclass_spellings(candidates)
|
|
175
|
+
return candidates if @superclass_nestings.empty?
|
|
176
|
+
|
|
177
|
+
known = candidates.each_with_object(Set.new) do |candidate, acc|
|
|
178
|
+
acc << candidate.class_name if candidate.class_name
|
|
179
|
+
acc.merge(candidate.class_shells || [])
|
|
180
|
+
end
|
|
181
|
+
rewrites = superclass_rewrites(known)
|
|
182
|
+
return candidates if rewrites.empty?
|
|
183
|
+
|
|
184
|
+
candidates.map do |candidate|
|
|
185
|
+
overlap = rewrites.slice(*candidate.class_superclasses.keys)
|
|
186
|
+
next candidate if overlap.empty?
|
|
187
|
+
|
|
188
|
+
rebuild_with_superclasses(candidate, candidate.class_superclasses.merge(overlap))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def superclass_rewrites(known)
|
|
193
|
+
@superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
|
|
194
|
+
next if token.nil? || nesting.empty?
|
|
195
|
+
|
|
196
|
+
resolved = resolve_superclass_token(token, nesting, known)
|
|
197
|
+
acc[full] = resolved if resolved && resolved != token
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The first candidate spelling that names a class this run declares or the RBS environment knows, or
|
|
202
|
+
# nil when none does — an unresolvable superclass is #735's business, not this pass's.
|
|
203
|
+
def resolve_superclass_token(token, nesting, known)
|
|
204
|
+
base = token.sub(/\[.*\]\z/, "")
|
|
205
|
+
args = token[base.length..] || ""
|
|
206
|
+
nesting.length.downto(1) do |i|
|
|
207
|
+
candidate = "#{nesting[0, i].join('::')}::#{base}"
|
|
208
|
+
return "#{candidate}#{args}" if known.include?(candidate) ||
|
|
209
|
+
Reflection.rbs_class_known?(candidate, environment: @environment)
|
|
210
|
+
end
|
|
211
|
+
token
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def rebuild_with_superclasses(candidate, superclasses)
|
|
215
|
+
MethodCandidate.new(
|
|
216
|
+
path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
|
|
217
|
+
kind: candidate.kind, classification: candidate.classification,
|
|
218
|
+
inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
|
|
219
|
+
rbs: candidate.rbs, skip_reason: candidate.skip_reason,
|
|
220
|
+
namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
|
|
221
|
+
class_superclasses: superclasses
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Issue #735 — a generated declaration whose superclass chain does not terminate in a class the RBS
|
|
226
|
+
# environment knows is DROPPED rather than emitted.
|
|
227
|
+
#
|
|
228
|
+
# `record_superclass` is right that a subclass declaration must carry its superclass: without it the
|
|
229
|
+
# sidecar misrepresents the class and dispatch degrades. But emitting one the environment cannot
|
|
230
|
+
# resolve is worse than either — `RBS::NoSuperclassFoundError` collapses the class, so every call into
|
|
231
|
+
# it reads `Dynamic[top]` and the whole declaration is a liability. On redmine, `class Principal <
|
|
232
|
+
# ApplicationRecord` (nothing declares `ApplicationRecord`, and nothing could: the chain ends at
|
|
233
|
+
# `ActiveRecord::Base`, which no RBS in the environment carries) collapsed 98 classes on the very next
|
|
234
|
+
# run. A chain that LOOPS is dropped for the same reason, and is the shape that made an
|
|
235
|
+
# `AncestorBuilder` walk recurse until `SystemStackError` (#609).
|
|
236
|
+
#
|
|
237
|
+
# Dropping is the conservative end: no declaration leaves the class exactly as it was before sig-gen
|
|
238
|
+
# ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
|
|
239
|
+
# OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
|
|
240
|
+
# every inherited call fires `call.undefined-method`.
|
|
241
|
+
def demote_unresolvable_superclasses(candidates)
|
|
242
|
+
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
243
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
|
|
244
|
+
end
|
|
245
|
+
# Everything this run declares, superclass or not: a chain that reaches `class Record` — emitted
|
|
246
|
+
# here, with no superclass of its own — terminates in a class the next run WILL know.
|
|
247
|
+
emitted = candidates.each_with_object(Set.new) do |candidate, acc|
|
|
248
|
+
acc << candidate.class_name if candidate.class_name
|
|
249
|
+
acc.merge(candidate.class_shells || [])
|
|
250
|
+
end
|
|
251
|
+
unresolvable = superclasses.each_with_object({}) do |(name, _sup), acc|
|
|
252
|
+
root = unresolved_superclass_root(name, superclasses, emitted)
|
|
253
|
+
acc[name] = root if root
|
|
254
|
+
end
|
|
255
|
+
return candidates if unresolvable.empty?
|
|
256
|
+
|
|
257
|
+
candidates.map do |candidate|
|
|
258
|
+
owner = unresolvable_for(candidate.class_name, unresolvable)
|
|
259
|
+
next candidate if owner.nil?
|
|
260
|
+
|
|
261
|
+
record_unresolvable_superclass(candidate, owner)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
|
|
266
|
+
# INSIDE `class WikiPage < ApplicationRecord`, so keeping the inner one while dropping the outer emits
|
|
267
|
+
# the unresolvable header anyway — the wrapper is not a candidate of its own and carries no methods,
|
|
268
|
+
# so nothing else would have demoted it.
|
|
269
|
+
def unresolvable_for(class_name, unresolvable)
|
|
270
|
+
return nil if class_name.nil?
|
|
271
|
+
return [class_name, unresolvable[class_name]] if unresolvable.key?(class_name)
|
|
272
|
+
|
|
273
|
+
unresolvable.each do |name, root|
|
|
274
|
+
return [name, root] if class_name.start_with?("#{name}::")
|
|
275
|
+
end
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# The first superclass token on `name`'s recorded chain that neither the RBS environment knows nor this
|
|
280
|
+
# run emits, or nil when the chain terminates in a known class. A chain that revisits a class returns
|
|
281
|
+
# that class's own superclass token — a cycle resolves nowhere.
|
|
282
|
+
def unresolved_superclass_root(name, superclasses, emitted, seen = {})
|
|
283
|
+
superclass = superclasses[name]
|
|
284
|
+
return nil if superclass.nil?
|
|
285
|
+
return superclass if seen[name]
|
|
286
|
+
|
|
287
|
+
seen[name] = true
|
|
288
|
+
# The recorded spelling may carry the type arguments {#generic_superclass_spelling} added; the
|
|
289
|
+
# resolvability question is about the class, so it is asked of the bare name.
|
|
290
|
+
base = superclass.sub(/\[.*\]\z/, "")
|
|
291
|
+
return nil if Reflection.rbs_class_known?(base, environment: @environment)
|
|
292
|
+
return superclass unless emitted.include?(base)
|
|
293
|
+
|
|
294
|
+
unresolved_superclass_root(base, superclasses, emitted, seen)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def record_unresolvable_superclass(candidate, owner)
|
|
298
|
+
class_name, root = owner
|
|
299
|
+
@unresolvable_superclasses[class_name] ||= root
|
|
300
|
+
MethodCandidate.new(
|
|
301
|
+
path: candidate.path, class_name: candidate.class_name, method_name: candidate.method_name,
|
|
302
|
+
kind: candidate.kind, classification: Classification::SKIPPED,
|
|
303
|
+
skip_reason: :unresolvable_superclass, inferred_return: candidate.inferred_return,
|
|
304
|
+
declared_return_rbs: candidate.declared_return_rbs,
|
|
305
|
+
namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
|
|
306
|
+
class_superclasses: candidate.class_superclasses
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
88
310
|
def build_environment
|
|
89
311
|
Environment.for_project(
|
|
90
312
|
libraries: @configuration.libraries,
|
|
@@ -193,7 +415,7 @@ module Rigor
|
|
|
193
415
|
|
|
194
416
|
full = (prefix + [name]).join("::")
|
|
195
417
|
@namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
|
|
196
|
-
record_superclass(node, full)
|
|
418
|
+
record_superclass(node, full, prefix)
|
|
197
419
|
walk_namespace_body(node, prefix + [name], out)
|
|
198
420
|
true
|
|
199
421
|
end
|
|
@@ -207,11 +429,38 @@ module Rigor
|
|
|
207
429
|
# superclass (`Class.new`, any other `CallNode`) is left unrecorded — a `Data.define` / `Struct.new` one is
|
|
208
430
|
# already carried by {#register_meta_classes}, and the rest are un-representable, where guessing would
|
|
209
431
|
# misfold.
|
|
210
|
-
def record_superclass(node, full)
|
|
432
|
+
def record_superclass(node, full, prefix)
|
|
211
433
|
return unless node.is_a?(Prism::ClassNode)
|
|
212
434
|
|
|
213
435
|
superclass = qualified_constant_path(node.superclass)
|
|
214
|
-
|
|
436
|
+
return if superclass.nil?
|
|
437
|
+
|
|
438
|
+
# Issue #722 — the nesting the header is WRITTEN in, kept whole-run (not per-file scratch) because
|
|
439
|
+
# the name it resolves to may be a class another file declares. Resolved once every file has been
|
|
440
|
+
# walked, in {#resolve_superclass_spellings}.
|
|
441
|
+
spelling = generic_superclass_spelling(superclass)
|
|
442
|
+
# The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
|
|
443
|
+
# file, while this table is whole-run.
|
|
444
|
+
@superclass_nestings[full] = [prefix.dup, spelling]
|
|
445
|
+
@class_superclasses[full] = spelling
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
|
|
449
|
+
# yields `RBS::InvalidTypeApplicationError: ::Array expects parameters [unchecked out E], but given
|
|
450
|
+
# args []`, which fails the build for that class exactly as an unresolvable superclass does — redmine
|
|
451
|
+
# subclasses `Array` five times. The element type is not something this pass can infer, so it is
|
|
452
|
+
# `untyped`: the subclass's OWN methods are what sig-gen is here to record, and `Array[untyped]` keeps
|
|
453
|
+
# the inherited surface resolving instead of collapsing the class.
|
|
454
|
+
def generic_superclass_spelling(superclass)
|
|
455
|
+
loader = @environment&.rbs_loader
|
|
456
|
+
return superclass if loader.nil?
|
|
457
|
+
|
|
458
|
+
arity = loader.class_type_param_names(superclass).size
|
|
459
|
+
return superclass if arity.zero?
|
|
460
|
+
|
|
461
|
+
"#{superclass}[#{(['untyped'] * arity).join(', ')}]"
|
|
462
|
+
rescue StandardError
|
|
463
|
+
superclass
|
|
215
464
|
end
|
|
216
465
|
|
|
217
466
|
# The ADR-48 member layouts for this file, in one table keyed by qualified class name. Reading the engine's
|
|
@@ -40,13 +40,8 @@ module Rigor
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
-
EMITTABLE = [Classification::NEW_FILE,
|
|
44
|
-
Classification::NEW_METHOD,
|
|
45
|
-
Classification::TIGHTER_RETURN].freeze
|
|
46
|
-
private_constant :EMITTABLE
|
|
47
|
-
|
|
48
43
|
def filter(candidates, selection)
|
|
49
|
-
active = selection.empty? ? EMITTABLE : selection
|
|
44
|
+
active = selection.empty? ? Classification::EMITTABLE : selection
|
|
50
45
|
candidates.select { |c| active.include?(c.classification) }
|
|
51
46
|
end
|
|
52
47
|
|
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
|
|
|
@@ -14,8 +14,18 @@ module Rigor
|
|
|
14
14
|
# * {.qualified_name_or_nil} is STRICT — a dynamic base anywhere in the chain yields `nil`, so a caller
|
|
15
15
|
# that statically names constants can treat the path as opaque rather than guessing.
|
|
16
16
|
#
|
|
17
|
-
# A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both policies
|
|
18
|
-
#
|
|
17
|
+
# A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both policies: the discovered-constant
|
|
18
|
+
# tables are keyed by un-rooted names, so a rendered `"::Foo"` would miss every one of them. The root
|
|
19
|
+
# marker is therefore carried OUT OF BAND by {.rooted?}, which a caller performing Ruby's lexical constant
|
|
20
|
+
# lookup MUST consult — `::Foo` names the top-level `Foo` and never a lexically nearer shadow
|
|
21
|
+
# ([#614](https://github.com/rigortype/rigor/issues/614)). A node that is neither a `ConstantReadNode`
|
|
22
|
+
# nor a `ConstantPathNode` yields `nil` under both policies.
|
|
23
|
+
#
|
|
24
|
+
# A `class` / `module` HEADER is a constant path too, and it has to consult the same marker — which is why
|
|
25
|
+
# {.declaration_prefix} and {.pushed_nesting} live here rather than in each of the two dozen walks that
|
|
26
|
+
# used to append the rendered name to their own prefix. `::` re-anchors a header at the top level exactly
|
|
27
|
+
# as it re-anchors a reference; reading only the rendered name filed `class ::Foo` inside `module MyApp`
|
|
28
|
+
# under `MyApp::Foo` ([#708](https://github.com/rigortype/rigor/issues/708)).
|
|
19
29
|
module ConstantPath
|
|
20
30
|
module_function
|
|
21
31
|
|
|
@@ -53,6 +63,73 @@ module Rigor
|
|
|
53
63
|
"#{parent_name}::#{node.name}"
|
|
54
64
|
end
|
|
55
65
|
end
|
|
66
|
+
|
|
67
|
+
# True when the reference is written with a leading `::` — `::Foo`, `::Foo::Bar`. Prism spells the
|
|
68
|
+
# root as a `ConstantPathNode` with a nil `parent`, so the answer lives at the LEFTMOST segment of the
|
|
69
|
+
# chain and the walk has to reach it: `::Foo::Bar` is a path node whose parent is itself a path node.
|
|
70
|
+
# A `ConstantReadNode` is a bare name and is never rooted; a dynamic base (`expr::Bar`) is not a root
|
|
71
|
+
# either, so the recursion stops at any non-constant parent.
|
|
72
|
+
def rooted?(node)
|
|
73
|
+
return false unless node.is_a?(Prism::ConstantPathNode)
|
|
74
|
+
|
|
75
|
+
parent = node.parent
|
|
76
|
+
return true if parent.nil?
|
|
77
|
+
|
|
78
|
+
rooted?(parent)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The qualified prefix a `class` / `module` header contributes to the body it opens, given the prefix of
|
|
82
|
+
# the body the header is WRITTEN in. `class Bar` inside `module Outer` is `Outer::Bar`; the compact
|
|
83
|
+
# `class Outer::Bar` written at the top level is the same name through one keyword instead of two.
|
|
84
|
+
#
|
|
85
|
+
# A ROOTED header RESETS the prefix. `class ::Rooted::Bar` names `Rooted::Bar` wherever it is written —
|
|
86
|
+
# Ruby's `::` re-anchors the header at the top level exactly as it does a reference — so the enclosing
|
|
87
|
+
# prefix is DROPPED rather than kept. Keeping it filed the class under `Outer::Rooted::Bar`, a name no
|
|
88
|
+
# caller writes, and the single-segment `class ::Foo` inside `module MyApp` became `MyApp::Foo`: not
|
|
89
|
+
# merely mis-keyed but undiscoverable, so a call on `Foo` typed opaque and reported nothing at all
|
|
90
|
+
# ([#708](https://github.com/rigortype/rigor/issues/708), [#638](https://github.com/rigortype/rigor/issues/638)).
|
|
91
|
+
#
|
|
92
|
+
# nil when the header names no constant — the same refusal {.qualified_name} makes, propagated so a
|
|
93
|
+
# caller keeps its own prefix rather than qualifying under a name it could not render.
|
|
94
|
+
def declaration_prefix(outer_prefix, constant_path)
|
|
95
|
+
name = qualified_name(constant_path)
|
|
96
|
+
return nil if name.nil?
|
|
97
|
+
|
|
98
|
+
rooted?(constant_path) ? [name] : outer_prefix + [name]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Ruby's `Module.nesting` inside the body a `class` / `module` header opens, given the nesting of the
|
|
102
|
+
# body the header is written in. ONE entry per declaration keyword, qualified against the entry already
|
|
103
|
+
# on top, so a compact `class A::B` contributes the single `"A::B"` where `module A; class B` contributes
|
|
104
|
+
# two — the whole difference between the two spellings, and unrecoverable from the `"A::B"` they share
|
|
105
|
+
# afterwards ([#652](https://github.com/rigortype/rigor/issues/652)). This is the ONLY thing that pushes
|
|
106
|
+
# an entry: a `def`, a block, and a `class << expr` body each inherit the chain unchanged, because none
|
|
107
|
+
# of them pushes a cref in Ruby.
|
|
108
|
+
#
|
|
109
|
+
# A ROOTED header pushes its name UNQUALIFIED and leaves the enclosing entries beneath it, which is the
|
|
110
|
+
# one place this differs from {.declaration_prefix}: Ruby's nesting inside `class ::Rooted::Bar` written
|
|
111
|
+
# in `module Outer` is `[Rooted::Bar, Outer]`, so the class's own name resets while `Outer` stays on the
|
|
112
|
+
# ladder as a live rung ([#708](https://github.com/rigortype/rigor/issues/708)).
|
|
113
|
+
#
|
|
114
|
+
# The header is rendered by the LENIENT {.qualified_name}, which is total over every header Ruby parses —
|
|
115
|
+
# `class` and `module` require a constant path, so the only dynamic base the grammar admits is `self`, and
|
|
116
|
+
# `class self::Thing` inside `module Outer` renders `"Thing"` and records `["Outer::Thing", "Outer"]`,
|
|
117
|
+
# which is what Ruby's nesting is there. So there is no unnameable-header case to refuse, and the `nil`
|
|
118
|
+
# guards below are reachable only for a non-constant node (which a `ClassNode` / `ModuleNode` header
|
|
119
|
+
# never is) and for the nil chain a caller may thread in.
|
|
120
|
+
#
|
|
121
|
+
# Both nesting walks call THIS function — `Inference::StatementEvaluator`'s declaration walk and
|
|
122
|
+
# `Inference::ScopeIndexer`'s def-node walk — so a callee re-walk answers what the body's own walk
|
|
123
|
+
# answers by construction rather than by two mirrored implementations staying in step.
|
|
124
|
+
def pushed_nesting(nesting, constant_path)
|
|
125
|
+
return nil if nesting.nil?
|
|
126
|
+
|
|
127
|
+
name = qualified_name(constant_path)
|
|
128
|
+
return nil if name.nil?
|
|
129
|
+
|
|
130
|
+
outer = rooted?(constant_path) ? nil : nesting.first
|
|
131
|
+
[outer ? "#{outer}::#{name}" : name, *nesting].freeze
|
|
132
|
+
end
|
|
56
133
|
end
|
|
57
134
|
end
|
|
58
135
|
end
|
|
@@ -327,7 +327,7 @@ module Rigor
|
|
|
327
327
|
parser == :toplevel ? d.method_name : parse_undefined_method(d)&.fetch(:method)
|
|
328
328
|
end
|
|
329
329
|
names.tally.sort_by { |method, count| [-count, method] }
|
|
330
|
-
|
|
330
|
+
.first(limit).map { |method, count| "#{method}×#{count}" }.join(" ")
|
|
331
331
|
end
|
|
332
332
|
|
|
333
333
|
def rule_of(diag)
|
|
@@ -369,6 +369,16 @@ module Rigor
|
|
|
369
369
|
StructInstance.new(members, class_name)
|
|
370
370
|
end
|
|
371
371
|
|
|
372
|
+
# ADR-3 amendment (ADR-20 slice 4) — generic Result[T, E] carrier constructor.
|
|
373
|
+
def result_of(ok_type, err_type)
|
|
374
|
+
Result.new(ok_type, err_type)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# ADR-3 amendment (ADR-20 slice 4) — generic Maybe[T] carrier constructor.
|
|
378
|
+
def maybe_of(value_type)
|
|
379
|
+
Maybe.new(value_type)
|
|
380
|
+
end
|
|
381
|
+
|
|
372
382
|
# Normalized union. Flattens nested Unions, deduplicates structurally equal members, drops Bot, and
|
|
373
383
|
# collapses 0/1-member results.
|
|
374
384
|
def union(*types)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../trinary"
|
|
4
|
+
require_relative "../value_semantics"
|
|
5
|
+
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Type
|
|
10
|
+
# A generic Maybe carrier representing an optional value that either contains a value of type
|
|
11
|
+
# `value_type` or nothing.
|
|
12
|
+
#
|
|
13
|
+
# Models `Maybe[T]` (e.g. `Dry::Monads::Maybe[T]`).
|
|
14
|
+
#
|
|
15
|
+
# Subtyping is covariant: `Maybe[T1] <: Maybe[T2]` iff `T1 <: T2`.
|
|
16
|
+
class Maybe
|
|
17
|
+
attr_reader :value_type
|
|
18
|
+
|
|
19
|
+
def initialize(value_type)
|
|
20
|
+
raise ArgumentError, "value_type must not be nil" if value_type.nil?
|
|
21
|
+
|
|
22
|
+
@value_type = value_type
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def describe(verbosity = :short)
|
|
27
|
+
"Maybe[#{value_type.describe(verbosity)}]"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def erase_to_rbs
|
|
31
|
+
"::Dry::Monads::Maybe[#{value_type.erase_to_rbs}]"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
include Rigor::Type::PlainLattice
|
|
35
|
+
|
|
36
|
+
include Rigor::Type::AcceptanceRouter
|
|
37
|
+
|
|
38
|
+
include Rigor::ValueSemantics
|
|
39
|
+
|
|
40
|
+
value_fields :value_type
|
|
41
|
+
|
|
42
|
+
def inspect
|
|
43
|
+
"#<Rigor::Type::Maybe #{describe(:short)}>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/rigor/type/refined.rb
CHANGED
|
@@ -77,14 +77,13 @@ module Rigor
|
|
|
77
77
|
# Recognises a Ruby value against this carrier's predicate. The trinary return is intentional:
|
|
78
78
|
# `true` / `false` when the predicate registry decides, `nil` when the predicate is unknown to the
|
|
79
79
|
# registry, so callers (today {Inference::Acceptance}) can fall through to gradual-mode `:maybe`.
|
|
80
|
-
# rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
80
|
+
# rubocop:disable-next Style/ReturnNilInPredicateMethodDefinition
|
|
81
81
|
def matches?(value)
|
|
82
82
|
recogniser = PREDICATES[predicate_id]
|
|
83
83
|
return nil if recogniser.nil?
|
|
84
84
|
|
|
85
85
|
!!recogniser.call(value)
|
|
86
86
|
end
|
|
87
|
-
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
88
87
|
|
|
89
88
|
# `predicate_id => recogniser` table. The recogniser is called with a Ruby value (typically the
|
|
90
89
|
# inner `value` of a `Constant`) and returns truthy when the value satisfies the predicate. The
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../trinary"
|
|
4
|
+
require_relative "../value_semantics"
|
|
5
|
+
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Type
|
|
10
|
+
# A generic Result carrier representing a computation that either succeeded with a value of type
|
|
11
|
+
# `ok_type` or failed with an error of type `err_type`.
|
|
12
|
+
#
|
|
13
|
+
# Models `Result[T, E]` (e.g. `Dry::Monads::Result[T, E]`).
|
|
14
|
+
#
|
|
15
|
+
# Subtyping is covariant in both positions: `Result[T1, E1] <: Result[T2, E2]` iff `T1 <: T2` and
|
|
16
|
+
# `E1 <: E2`.
|
|
17
|
+
class Result
|
|
18
|
+
attr_reader :ok_type, :err_type
|
|
19
|
+
|
|
20
|
+
alias success_type ok_type
|
|
21
|
+
alias failure_type err_type
|
|
22
|
+
|
|
23
|
+
def initialize(ok_type, err_type)
|
|
24
|
+
raise ArgumentError, "ok_type must not be nil" if ok_type.nil?
|
|
25
|
+
raise ArgumentError, "err_type must not be nil" if err_type.nil?
|
|
26
|
+
|
|
27
|
+
@ok_type = ok_type
|
|
28
|
+
@err_type = err_type
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def describe(verbosity = :short)
|
|
33
|
+
"Result[#{ok_type.describe(verbosity)}, #{err_type.describe(verbosity)}]"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def erase_to_rbs
|
|
37
|
+
"::Dry::Monads::Result[#{ok_type.erase_to_rbs}, #{err_type.erase_to_rbs}]"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
include Rigor::Type::PlainLattice
|
|
41
|
+
|
|
42
|
+
include Rigor::Type::AcceptanceRouter
|
|
43
|
+
|
|
44
|
+
include Rigor::ValueSemantics
|
|
45
|
+
|
|
46
|
+
value_fields :ok_type, :err_type
|
|
47
|
+
|
|
48
|
+
def inspect
|
|
49
|
+
"#<Rigor::Type::Result #{describe(:short)}>"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/rigor/type.rb
CHANGED
|
@@ -28,6 +28,8 @@ require_relative "type/difference"
|
|
|
28
28
|
require_relative "type/refined"
|
|
29
29
|
require_relative "type/intersection"
|
|
30
30
|
require_relative "type/app"
|
|
31
|
+
require_relative "type/result"
|
|
32
|
+
require_relative "type/maybe"
|
|
31
33
|
require_relative "type/bound_method"
|
|
32
34
|
require_relative "type/accepts_result"
|
|
33
35
|
require_relative "type/combinator"
|
data/lib/rigor/version.rb
CHANGED
|
@@ -56,7 +56,9 @@ module Rigor
|
|
|
56
56
|
# not ActionCable — pass through silently to avoid flagging unrelated `broadcast_to` methods.
|
|
57
57
|
return [] unless class_name.end_with?("Channel")
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
# `ChannelIndex#find` de-roots the query itself (#621) — a `::ChatChannel.broadcast_to` receiver
|
|
60
|
+
# and a plain `ChatChannel.broadcast_to` reach the same entry with no retry arm here.
|
|
61
|
+
entry = channel_index.find(class_name)
|
|
60
62
|
return [unknown_channel_violation(class_name, channel_index)] if entry.nil?
|
|
61
63
|
|
|
62
64
|
[broadcast_target_info(entry)]
|