rigortype 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -23,7 +23,9 @@ module Rigor
|
|
|
23
23
|
# - a statement, and the body of `(…)`, `begin` / `rescue` / `else` / `ensure`;
|
|
24
24
|
# - an assignment's value — every variable-write form, and an index `||=` / `&&=` / `op=`, but not its
|
|
25
25
|
# receiver or index arguments;
|
|
26
|
-
# - an `if` / `unless` / `case` predicate and every branch, but not a `when` condition or an `in` guard
|
|
26
|
+
# - an `if` / `unless` / `case` predicate and every branch, but not a `when` condition or an `in` guard (since
|
|
27
|
+
# issue #1963 the evaluator threads a `when` test that runs code in a `case` with a subject; the scan does
|
|
28
|
+
# not admit it yet, which floors a name needlessly rather than letting a pin through);
|
|
27
29
|
# - both operands of `&&` / `||`;
|
|
28
30
|
# - a `while` / `until` predicate and body, a `for` index, collection and body;
|
|
29
31
|
# - a multi-assign value and its local and instance-variable targets, and the LOCAL a `rescue => e`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "slot"
|
|
4
|
+
require_relative "parameters"
|
|
4
5
|
require_relative "../reflection"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
@@ -34,6 +35,26 @@ module Rigor
|
|
|
34
35
|
unreadable("definition_build_failed", [])
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# The block an otherwise defaulted inline declaration (`rigor:v1:inferred-signature`) declares, as a
|
|
39
|
+
# `{required, type}` slot, or nil. The rbs-inline plugin defaults an unannotated `&block` to `?{ (?) ->
|
|
40
|
+
# untyped }`, so any other block came from a `# @rbs &block:` annotation, and `check` binds the block's
|
|
41
|
+
# parameters from it. A block spelled like the default carries nothing either way.
|
|
42
|
+
def read_defaulted_block(owner, scope, name, lookup_scope)
|
|
43
|
+
method = lookup(owner, scope, name, lookup_scope)
|
|
44
|
+
defs = method ? method.defs.select { |definition| definition.defined_in.to_s.delete_prefix("::") == owner } : []
|
|
45
|
+
return nil unless defs.size == 1 && annotations_of(method, defs).include?(INFERRED_SIGNATURE)
|
|
46
|
+
|
|
47
|
+
block = defs.first.type.block
|
|
48
|
+
return nil if block.nil? || default_block?(block)
|
|
49
|
+
|
|
50
|
+
provenance, path = @provenance.classify(buffer_name(defs.first))
|
|
51
|
+
return nil if provenance.nil?
|
|
52
|
+
|
|
53
|
+
{ "required" => block.required, "type" => Slot.from_rbs(block_type(block), provenance, path) }
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
37
58
|
# The declared type of the constant `name` (qualified), as a slot with its declaration's provenance, or nil
|
|
38
59
|
# when no RBS or inline declaration names it.
|
|
39
60
|
def read_constant(name, environment)
|
|
@@ -102,14 +123,36 @@ module Rigor
|
|
|
102
123
|
def overload(method_type, provenance, path, return_inferred, annotations)
|
|
103
124
|
function = method_type.type
|
|
104
125
|
params = function.respond_to?(:required_positionals) ? params_of(function, provenance, path) : nil
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
partly_defaulted = return_inferred && provenance == "inline"
|
|
127
|
+
block = method_type.block && block_slot(method_type.block, provenance, path, partly_defaulted)
|
|
107
128
|
prefix = annotations.map { |string| "%a{#{string}} " }.join
|
|
108
129
|
return_slot = return_inferred ? nil : Slot.from_rbs(function.return_type.to_s, provenance, path)
|
|
109
130
|
{ "rbs" => "#{prefix}#{method_type}", "params" => params, "block" => block, "return" => return_slot,
|
|
110
131
|
"return_rbs" => function.return_type.to_s }
|
|
111
132
|
end
|
|
112
133
|
|
|
134
|
+
# A block's function type as `RBS::MethodType#to_s` spells it inside the braces (`(Integer) -> void`, with a
|
|
135
|
+
# `[self: T]` binding when declared). Neither `RBS::Types::Function` nor `UntypedFunction` has a `to_s`.
|
|
136
|
+
def block_type(block)
|
|
137
|
+
self_binding = ::RBS::Types::SelfTypeBindingHelper.self_type_binding_to_s(block.self_type)
|
|
138
|
+
"(#{block.type.param_to_s}) #{self_binding}-> #{block.type.return_to_s}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Under an inline declaration the rbs-inline plugin defaulted in part (`partly_defaulted`), a block shaped
|
|
142
|
+
# like its default was not written: it reads as an undeclared block does.
|
|
143
|
+
def block_slot(block, provenance, path, partly_defaulted)
|
|
144
|
+
if partly_defaulted && default_block?(block)
|
|
145
|
+
return { "required" => false, "type" => Slot.dynamic("inferred", Parameters::UNDECLARED) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
{ "required" => block.required, "type" => Slot.from_rbs(block_type(block), provenance, path) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def default_block?(block)
|
|
152
|
+
!block.required && block.type.is_a?(::RBS::Types::UntypedFunction) && block.self_type.nil? &&
|
|
153
|
+
block.type.return_type.is_a?(::RBS::Types::Bases::Any)
|
|
154
|
+
end
|
|
155
|
+
|
|
113
156
|
def params_of(function, provenance, path)
|
|
114
157
|
(positional_params(function) + keyword_params(function)).map do |kind, name, param|
|
|
115
158
|
{ "name" => name&.to_s, "kind" => kind, "type" => Slot.from_rbs(param.type.to_s, provenance, path) }
|
data/lib/rigor/lens/document.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require_relative "../version"
|
|
6
6
|
require_relative "../cache/engine_source"
|
|
7
7
|
require_relative "../analysis/run_cache_key"
|
|
8
|
+
require_relative "../rooted_glob"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Lens
|
|
@@ -60,7 +61,7 @@ module Rigor
|
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
def signature_digest
|
|
63
|
-
listing = signature_roots.flat_map { |root|
|
|
64
|
+
listing = signature_roots.flat_map { |root| RootedGlob.glob(root, "**/*.rbs") }.sort.map do |path|
|
|
64
65
|
"#{relative(path)}\t#{Lens.digest(File.binread(path))}\n"
|
|
65
66
|
end
|
|
66
67
|
Lens.digest(listing.join)
|
|
@@ -417,15 +417,25 @@ module Rigor
|
|
|
417
417
|
fields.each do |field, field_node|
|
|
418
418
|
# rbs-inline reads a `#:` trailing a field on its own line of a multi-line call as that field's type.
|
|
419
419
|
trailing = field_node.location.start_line != start_line
|
|
420
|
+
members = attribute_members(field, attribute)
|
|
421
|
+
visibility = builder_field_visibility(members)
|
|
420
422
|
add_site(field_node, { "kind" => "attribute", "name" => field, "owner" => name, "scope" => "instance",
|
|
421
|
-
"attribute" => attribute, "members" =>
|
|
422
|
-
"module_function" => false, "visibility" =>
|
|
423
|
+
"attribute" => attribute, "members" => members,
|
|
424
|
+
"module_function" => false, "visibility" => visibility, "params" => nil,
|
|
423
425
|
"block" => nil, "body_return" => nil }, contracts: trailing ? :trailing : false)
|
|
424
426
|
end
|
|
425
427
|
block = node.value.block
|
|
426
428
|
walk_body(block.body, nesting + [name], singleton: false, kind: :builder) if block&.body
|
|
427
429
|
end
|
|
428
430
|
|
|
431
|
+
# Ruby forces a field reader named `initialize_copy` (say) private, but not its `Struct` writer: a `Data`
|
|
432
|
+
# field reads `private`, a `Struct` one, whose site covers both, `null`.
|
|
433
|
+
def builder_field_visibility(members)
|
|
434
|
+
return "public" unless members.intersect?(FORCED_PRIVATE)
|
|
435
|
+
|
|
436
|
+
members.all? { |member| FORCED_PRIVATE.include?(member) } ? "private" : nil
|
|
437
|
+
end
|
|
438
|
+
|
|
429
439
|
# Whether a constant, class or module written here lands somewhere the nesting does not name: a builder
|
|
430
440
|
# block, which Ruby `class_eval`s (defs go to the new class, but constants stay with the lexical scope), or a
|
|
431
441
|
# `class << self` body (constants go to the singleton class). Such a declaration is an omission.
|
data/lib/rigor/lens/members.rb
CHANGED
|
@@ -14,10 +14,13 @@ module Rigor
|
|
|
14
14
|
# @param contracts — `{[owner, scope, name] => ContractReader::Contract or nil}`
|
|
15
15
|
# @param elsewhere — callable `(owner, scope, name) -> [{path, line}]`: other project files defining it
|
|
16
16
|
# @param site_paths — `{site key => absolute path of its file}`, to match an inline contract's buffer
|
|
17
|
-
|
|
17
|
+
# @param defaulted_blocks — `{[owner, scope, name] => {required, type}}`: the block a member's otherwise
|
|
18
|
+
# defaulted inline declaration declares (ContractReader#read_defaulted_block)
|
|
19
|
+
def initialize(sites:, omissions:, contracts:, elsewhere:, site_paths:, defaulted_blocks: {})
|
|
18
20
|
@sites = sites
|
|
19
21
|
@omissions = omissions
|
|
20
22
|
@contracts = contracts
|
|
23
|
+
@defaulted_blocks = defaulted_blocks
|
|
21
24
|
@elsewhere = elsewhere
|
|
22
25
|
@site_paths = site_paths
|
|
23
26
|
end
|
|
@@ -115,11 +118,22 @@ module Rigor
|
|
|
115
118
|
|
|
116
119
|
params, block, head = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
|
|
117
120
|
slot = inferred_return(single, scope)
|
|
118
|
-
|
|
121
|
+
declared = !site.data["alias_of"] && @defaulted_blocks[[site.data["owner"], scope, name]]
|
|
122
|
+
block = declared || block
|
|
123
|
+
block_rbs = block_spelling(block, declared)
|
|
119
124
|
{ "rbs" => "#{head}#{block_rbs} -> #{return_spelling(slot)}", "params" => params, "block" => block,
|
|
120
125
|
"return" => slot }
|
|
121
126
|
end
|
|
122
127
|
|
|
128
|
+
# A block an otherwise defaulted declaration declares reads as declared (`{ (Integer) -> void }`); one the
|
|
129
|
+
# definition merely takes reads as `?{ (?) -> untyped }`.
|
|
130
|
+
def block_spelling(block, declared)
|
|
131
|
+
return "" if block.nil?
|
|
132
|
+
return " ?{ (?) -> untyped }" unless declared
|
|
133
|
+
|
|
134
|
+
" #{'?' unless declared['required']}{ #{declared['type']['type']} }"
|
|
135
|
+
end
|
|
136
|
+
|
|
123
137
|
# An alias whose original Rigor cannot pin to one `def`: nothing about its signature is known.
|
|
124
138
|
def unknown_overload
|
|
125
139
|
{ "rbs" => nil, "params" => nil, "block" => nil, "return" => Slot.unavailable("not_computed") }
|
data/lib/rigor/lens/run.rb
CHANGED
|
@@ -78,6 +78,7 @@ module Rigor
|
|
|
78
78
|
def reset_captures
|
|
79
79
|
@captured = {}
|
|
80
80
|
@contracts = {}
|
|
81
|
+
@defaulted_blocks = {}
|
|
81
82
|
@site_paths = {}
|
|
82
83
|
end
|
|
83
84
|
|
|
@@ -145,18 +146,39 @@ module Rigor
|
|
|
145
146
|
if published
|
|
146
147
|
published_slot(published, scope.environment)
|
|
147
148
|
else
|
|
148
|
-
declared_slot(site, contract_reader(scope.environment).read_constant(name, scope.environment))
|
|
149
|
+
declared_slot(site, contract_reader(scope.environment).read_constant(name, scope.environment), scope)
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
# The rbs-inline plugin declares every constant of a file it reads, typing an unannotated one from its
|
|
153
154
|
# literal (`[]` as `Array[untyped]`), and reads only a `#:` trailing the assignment as its type: an inline
|
|
154
|
-
# declaration is the constant's own only when that is the site's one contract comment.
|
|
155
|
-
|
|
155
|
+
# declaration is the constant's own only when that is the site's one contract comment. Otherwise it is the
|
|
156
|
+
# plugin's default, which `check` reads all the same: that type, `inferred`. A `Class.new` / `Module.new`
|
|
157
|
+
# value is the exception, which `check` types through its own write reading, not the default `untyped`.
|
|
158
|
+
# An inline declaration from another file says nothing about this site: when the name is written in two
|
|
159
|
+
# files, the one the environment kept decides, and this site's own comment may say otherwise.
|
|
160
|
+
def declared_slot(site, declared, scope)
|
|
156
161
|
return Slot.unavailable("not_computed") if declared.nil?
|
|
157
|
-
return
|
|
162
|
+
return declared unless declared["provenance"] == "inline"
|
|
163
|
+
return Slot.unavailable("not_computed") unless own_inline_declaration?(site, scope)
|
|
164
|
+
return declared if trailing_type?(site)
|
|
165
|
+
return Slot.unavailable("not_computed") if anonymous_namespace?(site.node.value)
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
published_slot(scope.environment.constant_for_name(site.data["name"]), scope.environment)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def own_inline_declaration?(site, scope)
|
|
171
|
+
buffer = scope.environment.rbs_loader&.constant_declaration(site.data["name"])&.last
|
|
172
|
+
path = @site_paths.fetch(site.data["key"])
|
|
173
|
+
buffer&.end_with?(":#{path}") || false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def anonymous_namespace?(node)
|
|
177
|
+
return false unless node.is_a?(Prism::Node)
|
|
178
|
+
return true if node.is_a?(Prism::CallNode) && node.name == :new && node.receiver &&
|
|
179
|
+
%w[Class Module].include?(node.receiver.slice.delete_prefix("::"))
|
|
180
|
+
|
|
181
|
+
node.compact_child_nodes.any? { |child| anonymous_namespace?(child) }
|
|
160
182
|
end
|
|
161
183
|
|
|
162
184
|
def trailing_type?(site)
|
|
@@ -166,6 +188,7 @@ module Rigor
|
|
|
166
188
|
end
|
|
167
189
|
|
|
168
190
|
def published_slot(type, environment)
|
|
191
|
+
return Slot.unavailable("not_computed") if type.nil?
|
|
169
192
|
return Slot.dynamic("inferred", nil) if type.is_a?(Type::Dynamic)
|
|
170
193
|
|
|
171
194
|
spelled = speller(environment).call(type, nil)
|
|
@@ -178,7 +201,11 @@ module Rigor
|
|
|
178
201
|
key = [owner, member_scope, name]
|
|
179
202
|
return if @contracts.key?(key)
|
|
180
203
|
|
|
181
|
-
|
|
204
|
+
reader = contract_reader(scope.environment)
|
|
205
|
+
@contracts[key] = reader.read(owner, member_scope.to_sym, name, scope)
|
|
206
|
+
return if @contracts[key]
|
|
207
|
+
|
|
208
|
+
@defaulted_blocks[key] = reader.read_defaulted_block(owner, member_scope.to_sym, name, scope)
|
|
182
209
|
end
|
|
183
210
|
|
|
184
211
|
def target(entry, diagnostics, session)
|
|
@@ -229,7 +256,7 @@ module Rigor
|
|
|
229
256
|
sites = readers.values.flat_map(&:sites)
|
|
230
257
|
omissions = readers.values.flat_map(&:omissions)
|
|
231
258
|
requested = readers.keys.to_set
|
|
232
|
-
Members.new(sites: sites, omissions: omissions, contracts: @contracts,
|
|
259
|
+
Members.new(sites: sites, omissions: omissions, contracts: @contracts, defaulted_blocks: @defaulted_blocks,
|
|
233
260
|
elsewhere: elsewhere(session, requested), site_paths: @site_paths).build
|
|
234
261
|
end
|
|
235
262
|
|
data/lib/rigor/mcp/server.rb
CHANGED
|
@@ -128,8 +128,8 @@ module Rigor
|
|
|
128
128
|
properties: {
|
|
129
129
|
rule: {
|
|
130
130
|
type: "string",
|
|
131
|
-
description: "Rule ID, legacy alias, or family prefix (call, flow, assert, dump,
|
|
132
|
-
"Omit to list every rule."
|
|
131
|
+
description: "Rule ID, legacy alias, or family prefix (call, flow, def, assert, dump, global, " \
|
|
132
|
+
"suppression, static, effect). Omit to list every rule."
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
}
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "../cache/engine_source"
|
|
|
13
13
|
# plugin class body can be evaluated before anything else pulled the cache layer in.
|
|
14
14
|
require_relative "../cache/store"
|
|
15
15
|
require_relative "../source/node_walker"
|
|
16
|
+
require_relative "../rooted_glob"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
module Plugin
|
|
@@ -1003,7 +1004,7 @@ module Rigor
|
|
|
1003
1004
|
# plugin code calls `watch:`.
|
|
1004
1005
|
#
|
|
1005
1006
|
# @param roots — search roots (relative to the project root, or absolute paths)
|
|
1006
|
-
# @param patterns — glob suffixes
|
|
1007
|
+
# @param patterns — glob suffixes matched under each root, the root read literally (#1956).
|
|
1007
1008
|
# Multiple patterns union into one descriptor (`"**/*.erb", "**/*.html"` etc.).
|
|
1008
1009
|
def glob_descriptor(roots, *patterns)
|
|
1009
1010
|
files = collect_glob_files(Array(roots), patterns)
|
|
@@ -1163,7 +1164,7 @@ module Rigor
|
|
|
1163
1164
|
absolute = File.absolute_path(root.to_s)
|
|
1164
1165
|
next [] unless File.directory?(absolute)
|
|
1165
1166
|
|
|
1166
|
-
patterns.flat_map { |pattern|
|
|
1167
|
+
patterns.flat_map { |pattern| RootedGlob.glob(absolute, pattern) }
|
|
1167
1168
|
end
|
|
1168
1169
|
matched.uniq.sort.select { |path| File.file?(path) }
|
|
1169
1170
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "access_denied_error"
|
|
6
|
+
require_relative "../rooted_glob"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Plugin
|
|
@@ -33,6 +34,8 @@ module Rigor
|
|
|
33
34
|
# for a plugin that asks "which of these N candidate files exists here?" — rigor-actionpack tries
|
|
34
35
|
# nine template extensions per `render` call — where N per-path probe rows would put thousands of
|
|
35
36
|
# validations on every warm run and one listing row puts one per directory.
|
|
37
|
+
# - `#list_directory_kinds(path)` — the KIND-aware listing (#1959): each entry with its `lstat` kind, and one
|
|
38
|
+
# `:kinds` row that moves when an entry appears, vanishes or changes kind under the same name.
|
|
36
39
|
# - `#open_url(url)` — fetches the URL when the policy permits it (`network_policy: :allowlist` plus an
|
|
37
40
|
# `allowed_url_hosts` match) and raises {AccessDeniedError} otherwise. v0.1.2 ships the allowlist
|
|
38
41
|
# surface; the default project policy still has `network_policy: :disabled` so plugins that want network
|
|
@@ -189,7 +192,7 @@ module Rigor
|
|
|
189
192
|
# @return the absolute paths directly under `path` in `Dir.glob` order; empty when not a directory
|
|
190
193
|
def list_directory(path)
|
|
191
194
|
absolute = File.absolute_path(path.to_s)
|
|
192
|
-
entries =
|
|
195
|
+
entries = RootedGlob.glob(absolute, "*")
|
|
193
196
|
unless @policy.allow_read?(absolute)
|
|
194
197
|
record_refusal(absolute)
|
|
195
198
|
return entries
|
|
@@ -199,6 +202,34 @@ module Rigor
|
|
|
199
202
|
entries
|
|
200
203
|
end
|
|
201
204
|
|
|
205
|
+
# Issue #1959 — the KIND-aware listing. Returns each entry directly under `path` with what it is, a symlink
|
|
206
|
+
# not followed ({Cache::Descriptor::GlobEntry.lstat_kind}: `:file`, `:directory`, `:symlink` or `:other`, and
|
|
207
|
+
# `:absent` for an entry that vanished between the listing and its `lstat`),
|
|
208
|
+
# and records one `:kinds` {Cache::Descriptor::GlobEntry} over `path/*`. The row reads stale when an entry
|
|
209
|
+
# appears, disappears, or keeps its name and changes kind; NOT when an entry's bytes or stat tuple move, nor
|
|
210
|
+
# when anything below a subdirectory changes. It is the row for a walk that decides per top-level entry
|
|
211
|
+
# whether to descend: rigor-ffi skips `vendor/` and does not follow a symlinked directory, so a file `gen`
|
|
212
|
+
# replaced by a directory `gen/`, or a symlink replaced by a real directory, changes what it reads while
|
|
213
|
+
# every name stays put. {#list_directory}'s `:stat` row leaves directories out, and {#glob}'s `:names` row
|
|
214
|
+
# reads names alone.
|
|
215
|
+
#
|
|
216
|
+
# Like {#list_directory}, dot entries are not listed, and an out-of-policy directory is listed truthfully
|
|
217
|
+
# and contributes no row.
|
|
218
|
+
#
|
|
219
|
+
# @param path — project directory; relative paths expand against the working directory
|
|
220
|
+
# @return a Hash from each absolute entry path, in `Dir.glob` order, to its kind; empty when not a directory
|
|
221
|
+
def list_directory_kinds(path)
|
|
222
|
+
absolute = File.absolute_path(path.to_s)
|
|
223
|
+
entries = RootedGlob.glob(absolute, "*").to_h { |entry| [entry, Cache::Descriptor::GlobEntry.lstat_kind(entry)] }
|
|
224
|
+
unless @policy.allow_read?(absolute)
|
|
225
|
+
record_refusal(absolute)
|
|
226
|
+
return entries
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
record_glob_entry(absolute, "*", mode: :kinds)
|
|
230
|
+
entries
|
|
231
|
+
end
|
|
232
|
+
|
|
202
233
|
# Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
|
|
203
234
|
# matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
|
|
204
235
|
# reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
|
|
@@ -209,8 +240,13 @@ module Rigor
|
|
|
209
240
|
#
|
|
210
241
|
# The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
|
|
211
242
|
# brace (`{..,x}`) or a backslash escape (`.\\./`) raises {AccessDeniedError} (`:read_outside_scope`).
|
|
212
|
-
# The last two are refused outright because `Dir.glob` can spell `..` with them.
|
|
213
|
-
#
|
|
243
|
+
# The last two are refused outright because `Dir.glob` can spell `..` with them. A leading `/` is read under
|
|
244
|
+
# `root`, as the joined spelling always read it (`/lib/**/*.rb` is `root/lib/**/*.rb`). Out of policy, `root`
|
|
245
|
+
# is globbed truthfully and contributes no row, as in {#list_directory}.
|
|
246
|
+
#
|
|
247
|
+
# `root` is read literally (#1959): {RootedGlob} escapes its glob metacharacters before the join, so a root
|
|
248
|
+
# spelled `fx[1]` or a directory named `{..,zz}` neither matches a sibling nor leaves itself.
|
|
249
|
+
# {#list_directory} lists the same way, and {Cache::Descriptor::GlobEntry} re-globs the row the same way.
|
|
214
250
|
#
|
|
215
251
|
# @param root — project directory; relative paths expand against the working directory
|
|
216
252
|
# @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
|
|
@@ -227,7 +263,7 @@ module Rigor
|
|
|
227
263
|
)
|
|
228
264
|
end
|
|
229
265
|
|
|
230
|
-
entries =
|
|
266
|
+
entries = RootedGlob.glob(absolute, pattern)
|
|
231
267
|
unless @policy.allow_read?(absolute)
|
|
232
268
|
record_refusal(absolute)
|
|
233
269
|
return entries
|
|
@@ -390,8 +426,8 @@ module Rigor
|
|
|
390
426
|
self.class.attribute(entry)
|
|
391
427
|
end
|
|
392
428
|
|
|
393
|
-
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}
|
|
394
|
-
# deduplicated per (root, pattern, mode) slot.
|
|
429
|
+
# ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob} and the
|
|
430
|
+
# `:kinds` row for {#list_directory_kinds}), deduplicated per (root, pattern, mode) slot.
|
|
395
431
|
# Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
|
|
396
432
|
# carries the signature of the state the LAST reader saw.
|
|
397
433
|
def record_glob_entry(root, pattern, mode: :stat)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "box"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Plugin
|
|
@@ -74,8 +75,8 @@ module Rigor
|
|
|
74
75
|
# requiring the gem, which is what the caller is about to do.
|
|
75
76
|
def bundle_require_paths(bundle_root)
|
|
76
77
|
bundle_gem_dirs(bundle_root).flat_map do |gem_home|
|
|
77
|
-
specs =
|
|
78
|
-
|
|
78
|
+
specs = RootedGlob.glob(gem_home, "specifications/*.gemspec")
|
|
79
|
+
.filter_map { |file| Gem::Specification.load(file) }
|
|
79
80
|
specs.group_by(&:name)
|
|
80
81
|
.flat_map { |_, versions| versions.max_by(&:version).full_require_paths }
|
|
81
82
|
end
|
|
@@ -88,7 +89,7 @@ module Rigor
|
|
|
88
89
|
return [] if bundle_root.nil? || bundle_root.to_s.empty?
|
|
89
90
|
|
|
90
91
|
root = bundle_root.to_s
|
|
91
|
-
(
|
|
92
|
+
(RootedGlob.glob(root, "ruby/*") + [root])
|
|
92
93
|
.select { |dir| File.directory?(File.join(dir, "specifications")) }
|
|
93
94
|
end
|
|
94
95
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../inference/parameter_inference_collector"
|
|
4
4
|
require_relative "../inference/scope_indexer"
|
|
5
|
+
require_relative "../inference/project_seed_tables"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Protection
|
|
@@ -80,47 +81,19 @@ module Rigor
|
|
|
80
81
|
seed_tables(index).freeze
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
# The whole-project discovery tables, from the single-walk combined pass (one parse per file
|
|
84
|
-
#
|
|
85
|
-
# tables are dropped so an empty seed stays `{}` and every consumer's "seed nothing"
|
|
86
|
-
#
|
|
87
|
-
# `discovered_class_sources` is carried as the runner carries it on every run: Tier 2 never records edges,
|
|
88
|
-
# but an ancestor-order reader asks whether a class is declared in more than one file. The mixin tables
|
|
89
|
-
# (`discovered_includes`, `discovered_prepends`, `discovered_extends`) ride the same fold, so a singleton
|
|
90
|
-
# `extend` reads the same from a per-file scope under coverage and protection as it does under `check`.
|
|
84
|
+
# The whole-project discovery tables, from the single-walk combined pass (one parse per file) that
|
|
85
|
+
# {Analysis::Runner::ProjectPrePasses#discover} uses, mapped by the one list {Inference::ProjectSeedTables}
|
|
86
|
+
# keeps (issue #1614). Empty tables are dropped so an empty seed stays `{}` and every consumer's "seed nothing"
|
|
87
|
+
# branch keeps working.
|
|
91
88
|
def discovery_tables(paths)
|
|
92
89
|
seed_tables(Inference::ScopeIndexer.discovered_project_index_for_paths(paths))
|
|
93
90
|
end
|
|
94
91
|
|
|
95
|
-
# Maps a `{ classes:, def_index: }` discovery index onto the {Scope::DiscoveryIndex} slot names both
|
|
96
|
-
#
|
|
97
|
-
#
|
|
92
|
+
# Maps a `{ classes:, def_index: }` discovery index onto the {Scope::DiscoveryIndex} slot names both consumers
|
|
93
|
+
# take. Shared by the whole-walk ({#discovery_tables}) and bundle-fold ({#tables_for_buffer}) producers so a
|
|
94
|
+
# mutant's seed and the parent's seed can never disagree about shape.
|
|
98
95
|
def seed_tables(index)
|
|
99
|
-
|
|
100
|
-
tables = { discovered_classes: index.fetch(:classes) }
|
|
101
|
-
%i[
|
|
102
|
-
def_nodes def_nestings singleton_def_nodes def_sources singleton_def_sources superclasses includes
|
|
103
|
-
extends prepends unpositioned_mixins class_sources
|
|
104
|
-
method_visibilities methods parameter_envelopes data_member_layouts struct_member_layouts
|
|
105
|
-
deferred_ranges refinements global_write_census
|
|
106
|
-
].each do |slot|
|
|
107
|
-
tables[seed_key(slot)] = def_index.fetch(slot)
|
|
108
|
-
end
|
|
109
|
-
# ADR-119 WD1 — the pairs travel as one: an unpaired empty table is dropped here, a pair only when both
|
|
110
|
-
# halves are empty (`compact_pairs`), so a non-empty sibling is never stranded by an empty member.
|
|
111
|
-
tables.merge!(def_index.fetch(:siblings))
|
|
112
|
-
Scope::DiscoveryIndex.compact_pairs(
|
|
113
|
-
tables.reject { |name, table| !Scope::DiscoveryIndex.paired?(name) && (table.nil? || table.empty?) }
|
|
114
|
-
)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
UNPREFIXED_SLOTS = %i[data_member_layouts struct_member_layouts unpositioned_mixins].freeze
|
|
118
|
-
private_constant :UNPREFIXED_SLOTS
|
|
119
|
-
|
|
120
|
-
# The def-index slot names match the {Scope::DiscoveryIndex} ones under a `discovered_` prefix, except the
|
|
121
|
-
# two member-layout tables and the unpositioned-mixin table, which carry the same name on both sides.
|
|
122
|
-
def seed_key(slot)
|
|
123
|
-
UNPREFIXED_SLOTS.include?(slot) ? slot : :"discovered_#{slot}"
|
|
96
|
+
Inference::ProjectSeedTables.from_index(index)
|
|
124
97
|
end
|
|
125
98
|
end
|
|
126
99
|
end
|
|
@@ -209,7 +209,13 @@ module Rigor
|
|
|
209
209
|
def table_digest(project_scan)
|
|
210
210
|
payload = {
|
|
211
211
|
"synthetic" => project_scan.synthetic_method_index.to_h,
|
|
212
|
-
"patched" => patched_rows(project_scan.project_patched_methods)
|
|
212
|
+
"patched" => patched_rows(project_scan.project_patched_methods),
|
|
213
|
+
"patched_mixins" => project_scan.project_patched_methods.mixins.map do |mixin|
|
|
214
|
+
mixin.to_h.values.map(&:to_s)
|
|
215
|
+
end,
|
|
216
|
+
"patched_overrides" => project_scan.project_patched_methods.overrides.map do |key, visibility|
|
|
217
|
+
[*key, visibility].map(&:to_s)
|
|
218
|
+
end
|
|
213
219
|
}
|
|
214
220
|
Digest::SHA256.hexdigest(JSON.generate(payload))
|
|
215
221
|
rescue StandardError
|
|
@@ -221,7 +227,7 @@ module Rigor
|
|
|
221
227
|
def patched_rows(patched)
|
|
222
228
|
patched.by_key.map do |(class_name, method_name, kind), entry|
|
|
223
229
|
[class_name.to_s, method_name.to_s, kind.to_s, entry.source_path.to_s, entry.source_line,
|
|
224
|
-
entry.return_type&.describe(:short).to_s, entry.alias_of.to_s]
|
|
230
|
+
entry.return_type&.describe(:short).to_s, entry.alias_of.to_s, entry.visibility.to_s]
|
|
225
231
|
end.sort
|
|
226
232
|
end
|
|
227
233
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# `Dir.glob(File.join(root, pattern))` with `root` read as a literal path (#1956, #1959).
|
|
5
|
+
#
|
|
6
|
+
# Joining a directory into the pattern makes `Dir.glob` read the directory's own characters as glob syntax:
|
|
7
|
+
# a project at `ds [x]` or `ds{a}` matches nothing, `ds*` matches its siblings, `fx[1]` matches `fx1`, and
|
|
8
|
+
# a directory literally named `{..,zz}` expands into the parent tree. Each glob metacharacter of `root` is
|
|
9
|
+
# backslash-escaped before the join, so `root` matches only itself while `pattern` keeps exactly the meaning the
|
|
10
|
+
# joined spelling gave it: a leading `/` collapses into the join (`/etc/*` reads `root/etc/*`), and so does one
|
|
11
|
+
# inside a brace alternative (`{/etc,lib}/*` reads `root/etc/*` and `root/lib/*`). `Dir.glob(pattern, base:)`
|
|
12
|
+
# would not do that: it reads an absolute pattern, or an absolute alternative, outside `base`. A root without
|
|
13
|
+
# metacharacters is joined unchanged, so the results are the joined glob's strings in its order, `root` kept as
|
|
14
|
+
# given (relative, symlinked or with a trailing `/`).
|
|
15
|
+
module RootedGlob
|
|
16
|
+
METACHARACTERS = /[\[\]{}*?\\]/
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @param root — a directory path (String or Pathname), taken literally
|
|
21
|
+
# @param pattern — a `Dir.glob` pattern relative to `root`
|
|
22
|
+
# @return the matches spelled under `root`, in `Dir.glob` order; empty when `root` is not a directory
|
|
23
|
+
def glob(root, pattern)
|
|
24
|
+
Dir.glob(File.join(root.to_s.gsub(METACHARACTERS, "\\\\\\0"), pattern.to_s))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -231,6 +231,11 @@ module Rigor
|
|
|
231
231
|
# (the meta-new walk does not route `Parent` through ancestor discovery), so a reader that must place `K` in a
|
|
232
232
|
# hierarchy reads its superclass as unknown, as it reads the `nil` the table records for `class K < <expr>`.
|
|
233
233
|
ENVELOPE_META_SUPERCLASS_MARK = :"<meta-superclass>"
|
|
234
|
+
# Issue #1979 — the kind of a `[ENVELOPE_CONSTRUCTED_KIND, constant_name]` bucket entry: `class K < <expr>` whose
|
|
235
|
+
# superclass expression is a core constructor call (`Data.define(...)`, `Struct.new(...)`, `Class.new(...)`)
|
|
236
|
+
# naming that constant, as the receiver or as `Class.new`'s single bare-constant argument (`Object` for none).
|
|
237
|
+
# Read only by the #1596 proof's subclass walk (`RefinedDispatch::OwnMethod.each_subclass`).
|
|
238
|
+
ENVELOPE_CONSTRUCTED_KIND = :constructed_from
|
|
234
239
|
ENVELOPE_MODULE_MARK = :"<module>"
|
|
235
240
|
ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
|
|
236
241
|
ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
|
|
@@ -84,6 +84,20 @@ module Rigor
|
|
|
84
84
|
false
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# {.external_lacks?} on the side an entry stands for: an external superclass on a singleton chain
|
|
88
|
+
# (`:singleton`, its whole tail) is asked for a singleton method, an extended module (`:instance`) and every
|
|
89
|
+
# instance-side entry for an instance method.
|
|
90
|
+
def external_lacks_on?(scope, candidates, side, name)
|
|
91
|
+
return external_lacks?(scope, candidates, name) if side == :instance
|
|
92
|
+
|
|
93
|
+
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
94
|
+
return false if known.nil?
|
|
95
|
+
|
|
96
|
+
Rigor::Reflection.singleton_method_definition(known, name, scope: scope).nil?
|
|
97
|
+
rescue StandardError
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
87
101
|
# One verdict's working state: the scope, the resolver, and the edges the tests read.
|
|
88
102
|
class Context
|
|
89
103
|
attr_reader :mark, :edges
|