rigortype 0.4.4 → 0.4.6
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 +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- 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 +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -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 +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -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 +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -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/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- 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 +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- 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 +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -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/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- 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 +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- 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 +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -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 +59 -7
- 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 +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- 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/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- 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 +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -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-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- 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-activejob/lib/rigor/plugin/activejob.rb +4 -2
- 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-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- 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-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- 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/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module SigGen
|
|
@@ -27,12 +28,14 @@ module Rigor
|
|
|
27
28
|
class LayoutIndex
|
|
28
29
|
# The `[method_name, kind]` keys a member declaration answers for, `kind` being `:instance` or
|
|
29
30
|
# `:singleton`. A `def self?.f` answers for both kinds; an `attr_accessor` answers for its reader and its
|
|
30
|
-
# writer
|
|
31
|
+
# writer; an `alias new old` (`alias self.new self.old`) answers for its new name only, under its own kind
|
|
32
|
+
# (#1975). Shared with {Writer}, which asks the same question of the target file it is merging into.
|
|
31
33
|
def self.member_keys(member)
|
|
32
34
|
case member
|
|
33
35
|
when RBS::AST::Members::MethodDefinition
|
|
34
36
|
kinds = member.kind == :singleton_instance ? %i[instance singleton] : [member.kind]
|
|
35
37
|
kinds.map { |kind| [member.name, kind] }
|
|
38
|
+
when RBS::AST::Members::Alias then [[member.new_name, member.kind]]
|
|
36
39
|
when RBS::AST::Members::AttrReader then [[member.name, member.kind]]
|
|
37
40
|
when RBS::AST::Members::AttrWriter then [[:"#{member.name}=", member.kind]]
|
|
38
41
|
when RBS::AST::Members::AttrAccessor then [[member.name, member.kind], [:"#{member.name}=", member.kind]]
|
|
@@ -92,7 +95,7 @@ module Rigor
|
|
|
92
95
|
base = Pathname(dir)
|
|
93
96
|
next unless base.directory?
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
RootedGlob.glob(base, "**/*.rbs").each do |rbs_file|
|
|
96
99
|
index_file(Pathname(rbs_file), accumulator)
|
|
97
100
|
end
|
|
98
101
|
end
|
|
@@ -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
|
[]
|
|
@@ -90,10 +98,14 @@ module Rigor
|
|
|
90
98
|
|
|
91
99
|
base_scope = Scope.empty(environment: environment)
|
|
92
100
|
base_scope = base_scope.with_discovery(base_scope.discovery.with(discovered_classes: discovered_classes))
|
|
93
|
-
|
|
101
|
+
# Guard: sig-gen never reads a declared ivar type back (#1406). No spec pins this flag here, because
|
|
102
|
+
# it only matters for an RBS-declared class defined in an observed spec file.
|
|
103
|
+
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope,
|
|
104
|
+
declared_ivar_types: false)
|
|
94
105
|
bindings = collect_rspec_bindings(parse_result.value, scope_index)
|
|
106
|
+
@before_ivars = collect_before_ivars(parse_result.value, scope_index)
|
|
95
107
|
|
|
96
|
-
walk_calls(parse_result.value, scope_index, bindings, observations)
|
|
108
|
+
walk_calls(parse_result.value, scope_index, bindings, observations, [])
|
|
97
109
|
end
|
|
98
110
|
|
|
99
111
|
# Pre-walks `@source_paths` to collect every qualified class / module declaration. The result seeds the
|
|
@@ -145,22 +157,64 @@ module Rigor
|
|
|
145
157
|
end
|
|
146
158
|
end
|
|
147
159
|
|
|
148
|
-
|
|
160
|
+
# `blocks` is the chain of enclosing RSpec group / example / hook blocks, innermost last — the blocks a
|
|
161
|
+
# `before` ivar can reach the call from ({#before_ivar_type}). See {#enclosing_blocks} for where it is cut.
|
|
162
|
+
def walk_calls(node, scope_index, bindings, observations, blocks)
|
|
149
163
|
return unless node.is_a?(Prism::Node)
|
|
150
164
|
|
|
151
|
-
record_call(node, scope_index, bindings, observations) if node.is_a?(Prism::CallNode)
|
|
152
|
-
node.rigor_each_child
|
|
165
|
+
record_call(node, scope_index, bindings, observations, blocks) if node.is_a?(Prism::CallNode)
|
|
166
|
+
node.rigor_each_child do |child|
|
|
167
|
+
walk_calls(child, scope_index, bindings, observations, enclosing_blocks(node, child, blocks))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Group, example and hook calls whose block runs with the example's ivars. `RSpec.` may prefix a group call.
|
|
172
|
+
RSPEC_GROUP_CALLS = %i[
|
|
173
|
+
describe context feature shared_examples shared_examples_for shared_context
|
|
174
|
+
].freeze
|
|
175
|
+
RSPEC_EXAMPLE_OR_HOOK_CALLS = %i[
|
|
176
|
+
it specify example scenario its before prepend_before append_before
|
|
177
|
+
].freeze
|
|
178
|
+
private_constant :RSPEC_GROUP_CALLS, :RSPEC_EXAMPLE_OR_HOOK_CALLS
|
|
179
|
+
|
|
180
|
+
# The chain `child` of `parent` sees. A group / example / hook call's block extends it; every other block or
|
|
181
|
+
# lambda (`Class.new { … }`, `instance_eval { … }`, `define_method(:m) { … }`, `each { … }`), a class / module /
|
|
182
|
+
# singleton-class body, and a `def` that is not a direct statement of a group block cut it, since their `self`
|
|
183
|
+
# may be another object.
|
|
184
|
+
def enclosing_blocks(parent, child, blocks)
|
|
185
|
+
case child
|
|
186
|
+
when Prism::BlockNode then rspec_scope_call?(parent) ? [*blocks, child] : []
|
|
187
|
+
when Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then []
|
|
188
|
+
when Prism::DefNode then group_statement?(child, blocks.last) ? blocks : []
|
|
189
|
+
else blocks
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def rspec_scope_call?(node)
|
|
194
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
195
|
+
return rspec_group_receiver?(node.receiver) if RSPEC_GROUP_CALLS.include?(node.name)
|
|
196
|
+
|
|
197
|
+
node.receiver.nil? && RSPEC_EXAMPLE_OR_HOOK_CALLS.include?(node.name)
|
|
153
198
|
end
|
|
154
199
|
|
|
155
|
-
def
|
|
200
|
+
def rspec_group_receiver?(receiver)
|
|
201
|
+
receiver.nil? || (receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :RSpec)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def group_statement?(node, block)
|
|
205
|
+
body = block&.body
|
|
206
|
+
body.is_a?(Prism::StatementsNode) && body.body.any? { |statement| statement.equal?(node) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def record_call(call_node, scope_index, bindings, observations, blocks)
|
|
156
210
|
receiver = call_node.receiver
|
|
157
211
|
return if receiver.nil?
|
|
158
212
|
|
|
159
213
|
scope = scope_index[call_node] || scope_index[receiver]
|
|
160
214
|
return if scope.nil?
|
|
161
215
|
|
|
162
|
-
receiver_type = resolve_receiver_type(receiver, scope, bindings)
|
|
163
|
-
key = observation_key(call_node, receiver_type)
|
|
216
|
+
receiver_type = resolve_receiver_type(receiver, scope, bindings, blocks)
|
|
217
|
+
key = observation_key(call_node, observed_receiver(receiver_type, call_node, scope))
|
|
164
218
|
return if key.nil?
|
|
165
219
|
|
|
166
220
|
observation = collect_args(call_node, scope)
|
|
@@ -181,6 +235,36 @@ module Rigor
|
|
|
181
235
|
end
|
|
182
236
|
end
|
|
183
237
|
|
|
238
|
+
# Issue #1389 — a receiver typed `T | nil` whose one non-nil member is a `Nominal` or `Singleton` is observed as
|
|
239
|
+
# that member. A Minitest `setup` ivar read in a test method types as `Foo?` (setup is not `initialize`), and a
|
|
240
|
+
# call of a method `nil` does not answer can only have reached `Foo#bar` with its arguments: on `nil` it raises
|
|
241
|
+
# `NoMethodError`, and `&.` skips the call. A method `NilClass`'s RBS declares (`==`, `respond_to?`, `is_a?`)
|
|
242
|
+
# may have run on `nil` instead, so a plain `.` call of one keeps the union unobserved. The sole member still
|
|
243
|
+
# has to pass {#observation_key}'s `Nominal` / `Singleton` checks, so `untyped | nil` stays unobserved, and a
|
|
244
|
+
# union with two non-nil members (`Foo | Bar | nil`) passes through unchanged. Observation only: the
|
|
245
|
+
# receiver's inferred type elsewhere is untouched.
|
|
246
|
+
def observed_receiver(receiver_type, call_node, scope)
|
|
247
|
+
return receiver_type unless receiver_type.is_a?(Type::Union)
|
|
248
|
+
|
|
249
|
+
non_nil = receiver_type.members.reject { |member| nil_type?(member) }
|
|
250
|
+
return receiver_type unless non_nil.size == 1
|
|
251
|
+
return receiver_type if !call_node.safe_navigation? && nil_answers?(call_node.name, scope)
|
|
252
|
+
|
|
253
|
+
non_nil.first
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Whether `nil` may answer `method_name`: `NilClass`'s RBS declares it, or that RBS is not available to ask.
|
|
257
|
+
def nil_answers?(method_name, scope)
|
|
258
|
+
loader = scope.environment.rbs_loader
|
|
259
|
+
return true if loader.nil? || loader.instance_definition("NilClass").nil?
|
|
260
|
+
|
|
261
|
+
!loader.instance_method(class_name: "NilClass", method_name: method_name).nil?
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def nil_type?(type)
|
|
265
|
+
(type.is_a?(Type::Constant) && type.value.nil?) || (type.is_a?(Type::Nominal) && type.class_name == "NilClass")
|
|
266
|
+
end
|
|
267
|
+
|
|
184
268
|
# ADR-14 slice 5 — RSpec-aware receiver typing. Resolves a CallNode receiver against the collected
|
|
185
269
|
# `bindings` map (built by {#collect_rspec_bindings}) before falling back to ordinary `scope.type_of`. The
|
|
186
270
|
# three RSpec-shaped receivers we recognise:
|
|
@@ -190,11 +274,23 @@ module Rigor
|
|
|
190
274
|
# - `described_class.new(...)` chain — when the surrounding `describe Foo do … end` resolved `Foo`,
|
|
191
275
|
# return `Type::Nominal[Foo]`.
|
|
192
276
|
# - Anything else — pass through to `scope.type_of`, matching slice-3 behaviour.
|
|
193
|
-
def resolve_receiver_type(receiver, scope, bindings)
|
|
277
|
+
def resolve_receiver_type(receiver, scope, bindings, blocks)
|
|
194
278
|
return resolve_described_class_new(bindings) if described_class_new?(receiver)
|
|
195
279
|
return bindings[receiver.name] if bound_call?(receiver, bindings)
|
|
196
280
|
|
|
197
|
-
safe_type_of(scope, receiver)
|
|
281
|
+
type = safe_type_of(scope, receiver)
|
|
282
|
+
return type unless receiver.is_a?(Prism::InstanceVariableReadNode) && (type.nil? || type.is_a?(Type::Dynamic))
|
|
283
|
+
|
|
284
|
+
before_ivar_type(receiver.name, blocks) || type
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Issue #1389 — an ivar read inside an `it` block types as `Dynamic` (the block's ivars are not a class's), so
|
|
288
|
+
# a `before { @foo = Foo.new }` write stands in for it. Only a `before` whose example-group block encloses the
|
|
289
|
+
# read counts — a sibling `describe`, an un-included `shared_context` or a class body never reaches it — and
|
|
290
|
+
# when those `before`s write the name with more than one type, no type is answered.
|
|
291
|
+
def before_ivar_type(name, blocks)
|
|
292
|
+
types = blocks.flat_map { |block| @before_ivars.dig(block, name) || [] }.uniq
|
|
293
|
+
types.size == 1 ? types.first : nil
|
|
198
294
|
end
|
|
199
295
|
|
|
200
296
|
def bound_call?(receiver, bindings)
|
|
@@ -279,6 +375,39 @@ module Rigor
|
|
|
279
375
|
bindings[name] = body_type if body_type
|
|
280
376
|
end
|
|
281
377
|
|
|
378
|
+
# `before { @foo = Foo.new }` / `before(:each) do … end` inside an example-group block: records each ivar the
|
|
379
|
+
# `before` block's top-level statements write, with the written value's type, under that enclosing group
|
|
380
|
+
# block — `{ BlockNode => { :@foo => [Type] } }`, keyed by node identity. A `before` outside every group
|
|
381
|
+
# block ({#enclosing_blocks}), and `config.before`, record nothing.
|
|
382
|
+
def collect_before_ivars(root, scope_index)
|
|
383
|
+
index = {}.compare_by_identity
|
|
384
|
+
walk_before_ivars(root, [], index, scope_index)
|
|
385
|
+
index
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def walk_before_ivars(node, blocks, index, scope_index)
|
|
389
|
+
return unless node.is_a?(Prism::Node)
|
|
390
|
+
|
|
391
|
+
record_before_ivars(node, blocks.last, index, scope_index) if blocks.last
|
|
392
|
+
node.rigor_each_child do |child|
|
|
393
|
+
walk_before_ivars(child, enclosing_blocks(node, child, blocks), index, scope_index)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def record_before_ivars(node, group, index, scope_index)
|
|
398
|
+
return unless node.is_a?(Prism::CallNode) && node.name == :before && node.receiver.nil?
|
|
399
|
+
|
|
400
|
+
body = block_body_node(node.block)
|
|
401
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body].compact
|
|
402
|
+
statements.each do |statement|
|
|
403
|
+
next unless statement.is_a?(Prism::InstanceVariableWriteNode)
|
|
404
|
+
|
|
405
|
+
scope = scope_index[statement.value] || scope_index[statement]
|
|
406
|
+
type = scope && safe_type_of(scope, statement.value)
|
|
407
|
+
((index[group] ||= {})[statement.name] ||= []) << type if type
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
282
411
|
def binding_name_for(call_node)
|
|
283
412
|
first_arg = call_node.arguments&.arguments&.first
|
|
284
413
|
return call_node.name == :subject ? :subject : nil if first_arg.nil?
|
|
@@ -7,7 +7,7 @@ module Rigor
|
|
|
7
7
|
# Parses what sig-gen is about to emit, before it is emitted.
|
|
8
8
|
#
|
|
9
9
|
# Two rendering bugs have shipped RBS that `rbs` itself rejects — a non-identifier record key
|
|
10
|
-
# (`{
|
|
10
|
+
# (`{ "data-contrast": T }`) and a `&block` constructor parameter rendered before the parens
|
|
11
11
|
# (`(**untyped, ?{ (?) -> void })`). Both were found downstream, as a poisoned `sig/` tree: the file is
|
|
12
12
|
# quarantined, its types vanish, and (for a project adopting `reject-unparseable-signatures`) the build
|
|
13
13
|
# fails. A generator that can poison its own consumer's build is worth guarding at the source.
|
|
@@ -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,13 @@ 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
|
+
# @param diagnostics — the path errors `rigor check` would report for the arguments (#1991); JSON carries
|
|
33
|
+
# them as the top-level `diagnostics` key, and only when there are some.
|
|
34
|
+
def render(candidates:, mode:, format:, selection:, observation: nil, diagnostics: [])
|
|
30
35
|
case format
|
|
31
|
-
when "json" then render_json(filter(candidates, selection, with_skipped: true))
|
|
36
|
+
when "json" then render_json(filter(candidates, selection, with_skipped: true), observation, diagnostics)
|
|
32
37
|
when "text"
|
|
33
38
|
filtered = filter(candidates, selection)
|
|
34
39
|
mode == :diff ? render_diff(filtered) : render_print(filtered)
|
|
@@ -121,175 +126,31 @@ module Rigor
|
|
|
121
126
|
end
|
|
122
127
|
end
|
|
123
128
|
|
|
124
|
-
def render_json(candidates)
|
|
129
|
+
def render_json(candidates, observation = nil, diagnostics = [])
|
|
125
130
|
payload = { candidates: candidates.map(&:to_h) }
|
|
131
|
+
payload[:observation] = observation if observation
|
|
132
|
+
payload[:diagnostics] = diagnostics.map(&:to_h) if diagnostics.any?
|
|
126
133
|
@out.puts(JSON.pretty_generate(payload))
|
|
127
134
|
end
|
|
128
135
|
|
|
129
136
|
public
|
|
130
137
|
|
|
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.
|
|
138
|
+
# The per-source-file outcomes of a `--write` run, and the `--check` verdict, are reported by
|
|
139
|
+
# {WriteReporter}; these delegate so the command keeps one renderer.
|
|
136
140
|
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
|
|
141
|
+
WriteReporter.new(out: @out).render_write(results: results, format: format, refused: refused)
|
|
142
142
|
end
|
|
143
143
|
|
|
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
144
|
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
|
|
145
|
+
WriteReporter.new(out: @out).render_check(results: results, format: format, refused: refused)
|
|
160
146
|
end
|
|
161
147
|
|
|
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
148
|
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
|
|
149
|
+
WriteReporter.refusal_lines(refused)
|
|
173
150
|
end
|
|
174
151
|
|
|
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
152
|
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))
|
|
153
|
+
WriteReporter.out_of_date(results)
|
|
293
154
|
end
|
|
294
155
|
end
|
|
295
156
|
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
|
|
|
@@ -37,7 +37,17 @@ module Rigor
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# A set is mutable, so the classes the engine reads off its construction (`Set[x]`, `Set.new(xs)`) are not
|
|
41
|
+
# a contract: `x = Set[1]; x << "a"; x` holds a String. A written `Set` therefore keeps `untyped` for its
|
|
42
|
+
# element, as a Set constant does (#2177), however precisely the engine typed it.
|
|
43
|
+
SET_ELEMENT_UNTYPED = "Set"
|
|
44
|
+
private_constant :SET_ELEMENT_UNTYPED
|
|
45
|
+
|
|
40
46
|
def self.elaborate_nominal(type, environment, arity_cache)
|
|
47
|
+
if type.class_name == SET_ELEMENT_UNTYPED && type.type_args.size == 1
|
|
48
|
+
return Type::Combinator.nominal_of(type.class_name, type_args: [Type::Combinator.untyped])
|
|
49
|
+
end
|
|
50
|
+
|
|
41
51
|
elaborated_args = type.type_args.map { |arg| walk(arg, environment, arity_cache) }
|
|
42
52
|
return Type::Combinator.nominal_of(type.class_name, type_args: elaborated_args) if elaborated_args.any?
|
|
43
53
|
|
|
@@ -57,11 +67,17 @@ module Rigor
|
|
|
57
67
|
Type::Tuple.new(elements)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
type
|
|
70
|
+
# A bare generic in a value position (`{ k: Hash.new }`) fails `rbs validate` as a top-level one does
|
|
71
|
+
# (#2177), so the values are elaborated too. Only the pairs change; the key policy is carried over.
|
|
72
|
+
def self.elaborate_hash_shape(type, environment, arity_cache)
|
|
73
|
+
pairs = type.pairs.transform_values { |value| walk(value, environment, arity_cache) }
|
|
74
|
+
return type if pairs == type.pairs
|
|
75
|
+
|
|
76
|
+
Type::HashShape.new(
|
|
77
|
+
pairs,
|
|
78
|
+
required_keys: type.required_keys, optional_keys: type.optional_keys,
|
|
79
|
+
read_only_keys: type.read_only_keys, extra_keys: type.extra_keys
|
|
80
|
+
)
|
|
65
81
|
end
|
|
66
82
|
|
|
67
83
|
def self.generic_arity_for(class_name, environment, cache)
|