rigortype 0.4.2 → 0.4.4
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/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Lens
|
|
5
|
+
# Tagged type slots and RBS-buffer provenance (docs/internal-spec/lens.md § Slots).
|
|
6
|
+
module Slot
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def typed(type, provenance, path: nil, from_site: nil)
|
|
10
|
+
{ "state" => "typed", "type" => type, "provenance" => provenance, "path" => path, "from_site" => from_site }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def dynamic(provenance, cause, path: nil, from_site: nil)
|
|
14
|
+
{ "state" => "dynamic", "type" => "untyped", "provenance" => provenance, "path" => path,
|
|
15
|
+
"from_site" => from_site, "cause" => cause&.to_s }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unavailable(reason)
|
|
19
|
+
{ "state" => "unavailable", "reason" => reason }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unavailable?(slot)
|
|
23
|
+
slot.is_a?(Hash) && slot["state"] == "unavailable"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A slot for an RBS type string read from a contract. `untyped` is a dynamic slot whose cause is the
|
|
27
|
+
# authored contract.
|
|
28
|
+
def from_rbs(type, provenance, path)
|
|
29
|
+
return dynamic(provenance, :explicit_untyped, path: path) if type == "untyped"
|
|
30
|
+
|
|
31
|
+
typed(type, provenance, path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The RBS type a slot puts in an overload's `rbs` string: an unavailable slot reads `untyped`.
|
|
35
|
+
def rbs_spelling(slot)
|
|
36
|
+
slot["state"] == "typed" ? slot["type"] : "untyped"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Maps an RBS member's buffer name to a provenance (§ Slots).
|
|
41
|
+
class Provenance
|
|
42
|
+
CACHED_BUFFER = "<cached>"
|
|
43
|
+
|
|
44
|
+
# @param root — the project root (absolute)
|
|
45
|
+
# @param signature_roots — the project signature roots (absolute)
|
|
46
|
+
# @param plugin_roots — `{absolute signature directory => plugin id}` of the loaded plugins
|
|
47
|
+
def initialize(root:, signature_roots:, plugin_roots:)
|
|
48
|
+
@root = root
|
|
49
|
+
@signature_roots = signature_roots
|
|
50
|
+
@plugin_roots = plugin_roots
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# [provenance, path] for a buffer name, or nil when the buffer cannot be classified.
|
|
54
|
+
def classify(buffer_name)
|
|
55
|
+
name = buffer_name.to_s
|
|
56
|
+
return ["inline", nil] if name.start_with?("virtual:rbs-inline:")
|
|
57
|
+
return ["plugin:#{name.split(':', 3)[1]}", nil] if name.start_with?("virtual:")
|
|
58
|
+
return nil if name.empty? || name == CACHED_BUFFER
|
|
59
|
+
|
|
60
|
+
absolute = File.expand_path(name, @root)
|
|
61
|
+
plugin = @plugin_roots.find { |root, _| under?(absolute, [root]) }
|
|
62
|
+
return ["plugin:#{plugin.last}", nil] if plugin
|
|
63
|
+
return ["sig", relative(absolute)] if under?(absolute, @signature_roots)
|
|
64
|
+
|
|
65
|
+
["library", nil]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def relative(absolute)
|
|
69
|
+
Pathname(absolute).relative_path_from(Pathname(@root)).to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def under?(absolute, roots)
|
|
75
|
+
roots.any? { |root| absolute == root || absolute.start_with?("#{root}/") }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/rigor/lens.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hashing/xxh3"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
# `rigor lens`: a declaration map of the requested files with type provenance, computed as `rigor check` computes
|
|
7
|
+
# them (ADR-113; the output contract is docs/internal-spec/lens.md).
|
|
8
|
+
module Lens
|
|
9
|
+
SCHEMA = "lens/v0"
|
|
10
|
+
# Bumped on every breaking change to the `lens/v0` document (docs/internal-spec/lens.md § Status).
|
|
11
|
+
REVISION = 1
|
|
12
|
+
CAPABILITIES = %w[class module method attribute constant module_function builder alias body_return
|
|
13
|
+
diagnostics inputs].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# A Prism location as the document's span object: 1-based lines, 0-based byte columns and offsets.
|
|
18
|
+
def span(location)
|
|
19
|
+
{ "start_line" => location.start_line, "start_column" => location.start_column,
|
|
20
|
+
"end_line" => location.end_line, "end_column" => location.end_column,
|
|
21
|
+
"start_offset" => location.start_offset, "end_offset" => location.end_offset }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The span from the first to the last of a site's contract comments, or nil when it has none.
|
|
25
|
+
def contract_span(comments)
|
|
26
|
+
return nil if comments.empty?
|
|
27
|
+
|
|
28
|
+
first = comments.first.location
|
|
29
|
+
last = comments.last.location
|
|
30
|
+
{ "start_line" => first.start_line, "start_column" => first.start_column, "end_line" => last.end_line,
|
|
31
|
+
"end_column" => last.end_column, "start_offset" => first.start_offset, "end_offset" => last.end_offset }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def digest(bytes)
|
|
35
|
+
"xxh3-64:#{Hashing::XXH3.file_hash(bytes)}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
require_relative "lens/run"
|
|
41
|
+
require_relative "lens/document"
|
data/lib/rigor/mcp/server.rb
CHANGED
|
@@ -63,6 +63,25 @@ module Rigor
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: "rigor_lens",
|
|
68
|
+
description: "Map the classes, modules, methods and attributes the given Ruby files declare, with the " \
|
|
69
|
+
"type `rigor check` gives each one and where that type came from (sig/, an inline " \
|
|
70
|
+
"comment, a plugin, a library, or inference). Returns the lens/v0 JSON document " \
|
|
71
|
+
"(experimental). Pass every file you need in one call: each call pays one analysis run.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
files: {
|
|
76
|
+
type: "array",
|
|
77
|
+
items: { type: "string" },
|
|
78
|
+
description: "Ruby files to map (required)"
|
|
79
|
+
},
|
|
80
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
81
|
+
},
|
|
82
|
+
required: ["files"]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
66
85
|
{
|
|
67
86
|
name: "rigor_annotate",
|
|
68
87
|
description: "Return the given Ruby source file with each line's last-expression type " \
|
|
@@ -180,13 +199,12 @@ module Rigor
|
|
|
180
199
|
out_io = StringIO.new
|
|
181
200
|
err_io = StringIO.new
|
|
182
201
|
require_relative "../cli"
|
|
183
|
-
exit_code = CLI.new(argv, out: out_io, err: err_io).run
|
|
184
|
-
|
|
185
|
-
is_error = exit_code == CLI::EXIT_USAGE
|
|
186
|
-
text = out_io.string
|
|
187
|
-
text = err_io.string if text.empty? && is_error
|
|
202
|
+
exit_code = with_captured_streams(out_io, err_io) { CLI.new(argv, out: out_io, err: err_io).run }
|
|
188
203
|
|
|
189
|
-
success(id,
|
|
204
|
+
success(id, tool_result(out_io.string, err_io.string, exit_code))
|
|
205
|
+
rescue SystemExit => e
|
|
206
|
+
# Defence against any command that exits: it must not take the server down with it.
|
|
207
|
+
success(id, { content: [{ type: "text", text: "Command exited (status #{e.status})" }], isError: true })
|
|
190
208
|
rescue StandardError => e
|
|
191
209
|
@err.puts("rigor mcp: #{e.class}: #{e.message}")
|
|
192
210
|
@err.puts(e.backtrace.first(5).join("\n")) if e.backtrace
|
|
@@ -196,6 +214,26 @@ module Rigor
|
|
|
196
214
|
})
|
|
197
215
|
end
|
|
198
216
|
|
|
217
|
+
# A non-zero exit with nothing on stdout printed only an error; exit 1 with a document (check, lens) is a
|
|
218
|
+
# result, not an error.
|
|
219
|
+
def tool_result(out, err, exit_code)
|
|
220
|
+
is_error = [CLI::EXIT_USAGE, CLI::EXIT_INTERNAL_ERROR].include?(exit_code) || (out.empty? && !exit_code.zero?)
|
|
221
|
+
{ content: [{ type: "text", text: out.empty? ? err : out }], isError: is_error }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The JSON-RPC channel is the process's stdout, so nothing a command writes past its `out:` (an option
|
|
225
|
+
# parser's `--help`, a stray `puts`) may reach it while the command runs.
|
|
226
|
+
def with_captured_streams(out_io, err_io)
|
|
227
|
+
stdout = $stdout
|
|
228
|
+
stderr = $stderr
|
|
229
|
+
$stdout = out_io
|
|
230
|
+
$stderr = err_io
|
|
231
|
+
yield
|
|
232
|
+
ensure
|
|
233
|
+
$stdout = stdout
|
|
234
|
+
$stderr = stderr
|
|
235
|
+
end
|
|
236
|
+
|
|
199
237
|
def build_argv(name, args) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
200
238
|
effective_config = args["config"] || @config_path
|
|
201
239
|
|
|
@@ -203,49 +241,52 @@ module Rigor
|
|
|
203
241
|
when "rigor_check"
|
|
204
242
|
argv = ["check", "--format=json", "--no-stats"]
|
|
205
243
|
argv << "--config=#{effective_config}" if effective_config
|
|
206
|
-
argv
|
|
207
|
-
argv
|
|
244
|
+
argv + ["--", *Array(args["paths"])]
|
|
208
245
|
|
|
209
246
|
when "rigor_type_of"
|
|
210
247
|
return nil unless args["file"] && args["line"] && args["col"]
|
|
211
248
|
|
|
212
249
|
argv = ["type-of", "--format=json"]
|
|
213
250
|
argv << "--config=#{effective_config}" if effective_config
|
|
214
|
-
|
|
215
|
-
argv
|
|
251
|
+
# `--` ends the options, so a file named like a flag is a file.
|
|
252
|
+
argv + ["--", "#{args['file']}:#{args['line']}:#{args['col']}"]
|
|
216
253
|
|
|
217
254
|
when "rigor_triage"
|
|
218
255
|
argv = ["triage", "--format=json"]
|
|
219
256
|
argv << "--config=#{effective_config}" if effective_config
|
|
220
257
|
argv << "--top=#{args['top']}" if args["top"]
|
|
221
|
-
argv
|
|
222
|
-
|
|
258
|
+
argv + ["--", *Array(args["paths"])]
|
|
259
|
+
|
|
260
|
+
when "rigor_lens"
|
|
261
|
+
files = args["files"]
|
|
262
|
+
return nil unless files.is_a?(Array) && !files.empty? && files.all?(String)
|
|
263
|
+
|
|
264
|
+
argv = ["lens", "--format=json"]
|
|
265
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
266
|
+
# `--` ends the options, so a file named like a flag (`--version`) is a file.
|
|
267
|
+
argv + ["--", *files]
|
|
223
268
|
|
|
224
269
|
when "rigor_annotate"
|
|
225
270
|
return nil unless args["file"]
|
|
226
271
|
|
|
227
272
|
argv = ["annotate", "--no-color"]
|
|
228
273
|
argv << "--config=#{effective_config}" if effective_config
|
|
229
|
-
argv
|
|
230
|
-
argv
|
|
274
|
+
argv + ["--", args["file"]]
|
|
231
275
|
|
|
232
276
|
when "rigor_sig_gen"
|
|
233
277
|
argv = ["sig-gen", "--print", "--format=json"]
|
|
234
278
|
argv << "--config=#{effective_config}" if effective_config
|
|
235
279
|
argv << "--params=#{args['params']}" if args["params"]
|
|
236
|
-
argv
|
|
237
|
-
argv
|
|
280
|
+
argv + ["--", *Array(args["paths"])]
|
|
238
281
|
|
|
239
282
|
when "rigor_explain"
|
|
240
283
|
argv = ["explain", "--format=json"]
|
|
241
|
-
|
|
242
|
-
argv
|
|
284
|
+
args["rule"] ? argv + ["--", args["rule"]] : argv
|
|
243
285
|
|
|
244
286
|
when "rigor_coverage"
|
|
245
287
|
argv = ["coverage", "--format=json"]
|
|
246
288
|
argv << "--config=#{effective_config}" if effective_config
|
|
247
|
-
argv
|
|
248
|
-
argv
|
|
289
|
+
argv + ["--", *Array(args["paths"])]
|
|
249
290
|
end
|
|
250
291
|
end
|
|
251
292
|
|
|
@@ -15,7 +15,8 @@ module Rigor
|
|
|
15
15
|
# under `RUBY_BOX=1` and must print the right answer. That admits a 4.1.0dev build with the fix and a
|
|
16
16
|
# future 4.0.x that backports it, and refuses a development build cut before the fix — none of which a
|
|
17
17
|
# `RUBY_VERSION` bound gets right. The probe costs one short-lived `ruby --disable-gems` process, paid
|
|
18
|
-
# only when a run opts into `ruby_box
|
|
18
|
+
# only when a run opts into `ruby_box` or starts with `RUBY_BOX` already set, and only once per Ruby
|
|
19
|
+
# build: {cached_unsupported_reason} remembers the verdict.
|
|
19
20
|
#
|
|
20
21
|
# Loaded by `exe/rigor` before anything else, so this file requires nothing from Rigor.
|
|
21
22
|
module BoxProbe
|
|
@@ -36,20 +37,166 @@ module Rigor
|
|
|
36
37
|
# counted as a failure, so a wedged probe cannot hang the launcher (or `rigor lsp` behind it).
|
|
37
38
|
DEADLINE_SECONDS = 10
|
|
38
39
|
|
|
40
|
+
# Set on the environment of every re-exec this module asks for, so the re-exec'ed launcher neither
|
|
41
|
+
# probes again nor re-execs again. The launcher deletes it before Rigor loads, so nothing Rigor spawns
|
|
42
|
+
# inherits it.
|
|
43
|
+
REEXEC_MARKER = "RIGOR_BOX_PROBED"
|
|
44
|
+
|
|
45
|
+
# The signals the bug's NULL-box dereference ends a child with. A child killed by any other signal
|
|
46
|
+
# (an OOM killer's `SIGKILL`, an operator's `SIGTERM`) is still refused, but that verdict is not cached.
|
|
47
|
+
CRASH_SIGNALS = %w[SEGV BUS ABRT ILL].freeze
|
|
48
|
+
|
|
49
|
+
CACHE_FORMAT = "rigor-box-probe-v1"
|
|
50
|
+
|
|
39
51
|
module_function
|
|
40
52
|
|
|
53
|
+
# Decides what `exe/rigor` does before Rigor loads, editing `env` (the real `ENV` in the launcher) in
|
|
54
|
+
# place. Returns true when the launcher must re-exec itself with `env` as now edited.
|
|
55
|
+
#
|
|
56
|
+
# - `RUBY_BOX` already set (by hand or by a wrapper), and `Ruby::Box` active in this process: the
|
|
57
|
+
# process booted inside the box before any Rigor code ran, so on a Ruby without the Bug #22260 fix
|
|
58
|
+
# the first Ractor-shareable lambda Rigor calls segfaults. Probe (through the cache); when the Ruby
|
|
59
|
+
# is refused, print a notice, drop `RUBY_BOX` and the `ruby_box` request, and re-exec. When it
|
|
60
|
+
# passes, keep `RUBY_BOX` as the user set it.
|
|
61
|
+
# - `ruby_box` requested and `RUBY_BOX` unset: probe; re-exec under `RUBY_BOX=1` when it passes,
|
|
62
|
+
# otherwise warn and drop the request.
|
|
63
|
+
# - Otherwise nothing.
|
|
64
|
+
def needs_reexec?(env, stderr: $stderr, box_active: box_active?, cache_dir: default_cache_dir(env))
|
|
65
|
+
return false if env.delete(REEXEC_MARKER)
|
|
66
|
+
|
|
67
|
+
if env["RUBY_BOX"].to_s.empty?
|
|
68
|
+
reexec_for_box_request?(env, stderr, cache_dir)
|
|
69
|
+
else
|
|
70
|
+
reexec_without_preset_box?(env, stderr, box_active, cache_dir)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reexec_for_box_request?(env, stderr, cache_dir)
|
|
75
|
+
isolation = env["RIGOR_PLUGIN_ISOLATION"].to_s
|
|
76
|
+
return false unless isolation == "ruby_box" || !env["RIGOR_BOX"].to_s.empty?
|
|
77
|
+
|
|
78
|
+
if (reason = cached_unsupported_reason(cache_dir: cache_dir))
|
|
79
|
+
# `puts` on stderr, not `warn`: `-W0` silences `warn`, and this fallback must never be silent.
|
|
80
|
+
stderr.puts "rigor: ruby_box plugin isolation is unavailable: #{reason}. " \
|
|
81
|
+
"Falling back to the configured plugins_isolation strategy (default: process)."
|
|
82
|
+
drop_box_request(env)
|
|
83
|
+
false
|
|
84
|
+
else
|
|
85
|
+
env["RUBY_BOX"] = "1"
|
|
86
|
+
env["RIGOR_PLUGIN_ISOLATION"] = "ruby_box" if isolation.empty?
|
|
87
|
+
env[REEXEC_MARKER] = "1"
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reexec_without_preset_box?(env, stderr, box_active, cache_dir)
|
|
93
|
+
return false unless box_active
|
|
94
|
+
|
|
95
|
+
reason = cached_unsupported_reason(cache_dir: cache_dir)
|
|
96
|
+
return false unless reason
|
|
97
|
+
|
|
98
|
+
stderr.puts "rigor: RUBY_BOX is set, but #{reason}. " \
|
|
99
|
+
"Re-running without RUBY_BOX; plugins will not run inside Ruby::Box."
|
|
100
|
+
env.delete("RUBY_BOX")
|
|
101
|
+
drop_box_request(env)
|
|
102
|
+
env[REEXEC_MARKER] = "1"
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def drop_box_request(env)
|
|
107
|
+
env.delete("RIGOR_PLUGIN_ISOLATION") if env["RIGOR_PLUGIN_ISOLATION"].to_s == "ruby_box"
|
|
108
|
+
env.delete("RIGOR_BOX")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Whether this process booted inside `Ruby::Box`. False on a Ruby without the feature, which ignores
|
|
112
|
+
# `RUBY_BOX`, so a preset variable there costs nothing.
|
|
113
|
+
def box_active?
|
|
114
|
+
return false unless defined?(::Ruby::Box) && ::Ruby::Box.respond_to?(:enabled?)
|
|
115
|
+
|
|
116
|
+
::Ruby::Box.enabled? == true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `$XDG_CACHE_HOME/rigor` when that is an absolute path, else `$HOME/.cache/rigor` when `HOME` is an
|
|
120
|
+
# absolute path (the passwd entry when `HOME` is unset); nil otherwise, which turns the cache off
|
|
121
|
+
# rather than writing under the working directory.
|
|
122
|
+
def default_cache_dir(env)
|
|
123
|
+
xdg = env["XDG_CACHE_HOME"].to_s
|
|
124
|
+
return File.join(xdg, "rigor") if xdg.start_with?("/")
|
|
125
|
+
|
|
126
|
+
home = env.key?("HOME") ? env["HOME"].to_s : Dir.home
|
|
127
|
+
File.absolute_path?(home) ? File.join(home, ".cache", "rigor") : nil
|
|
128
|
+
rescue ArgumentError
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# {unsupported_reason}, remembered per Ruby build so a launcher that probes on every run pays for the
|
|
133
|
+
# child process once. A pass is stored only when the child printed the right answer, and a refusal
|
|
134
|
+
# only when it is a property of the Ruby (no `Ruby::Box`, or a crash signal); a probe that could not
|
|
135
|
+
# run, timed out, or failed some other way is answered but not stored. An unreadable or corrupt
|
|
136
|
+
# cache file reads as a miss; a write that fails is ignored.
|
|
137
|
+
def cached_unsupported_reason(ruby = RbConfig.ruby, cache_dir:)
|
|
138
|
+
path = cache_dir && cache_path(cache_dir, ruby)
|
|
139
|
+
cached = path && read_cache(path)
|
|
140
|
+
return cached[:reason] if cached
|
|
141
|
+
|
|
142
|
+
reason, cacheable = verdict(ruby)
|
|
143
|
+
write_cache(path, reason) if path && cacheable
|
|
144
|
+
reason
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The cache file for `ruby`: keyed on the Ruby build that runs the launcher (its version, patch level,
|
|
148
|
+
# revision and description) together with the executable path and the probe script itself.
|
|
149
|
+
def cache_path(cache_dir, ruby)
|
|
150
|
+
require "digest/sha2"
|
|
151
|
+
key = [RUBY_VERSION, RUBY_PATCHLEVEL, RUBY_REVISION, RUBY_DESCRIPTION, File.expand_path(ruby), SCRIPT]
|
|
152
|
+
File.join(cache_dir, "ruby-box-probe", Digest::SHA256.hexdigest(key.join("\0")))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def read_cache(path)
|
|
156
|
+
format, verdict, reason = File.read(path).split("\n", 3)
|
|
157
|
+
return nil unless format == CACHE_FORMAT
|
|
158
|
+
|
|
159
|
+
case verdict
|
|
160
|
+
when "pass" then reason.nil? ? { reason: nil } : nil
|
|
161
|
+
when "fail" then reason.to_s.chomp.empty? ? nil : { reason: reason.chomp }
|
|
162
|
+
end
|
|
163
|
+
rescue SystemCallError, IOError, ArgumentError
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def write_cache(path, reason)
|
|
168
|
+
require "fileutils"
|
|
169
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
170
|
+
body = reason ? "#{CACHE_FORMAT}\nfail\n#{reason}\n" : "#{CACHE_FORMAT}\npass"
|
|
171
|
+
temp = "#{path}.#{::Process.pid}.tmp"
|
|
172
|
+
File.write(temp, body)
|
|
173
|
+
File.rename(temp, path)
|
|
174
|
+
rescue SystemCallError, IOError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
41
178
|
# nil when `ruby` can run Rigor under `RUBY_BOX=1`; otherwise a short reason for the launcher's
|
|
42
179
|
# warning. Only a child killed by a signal is blamed on the bug; any other failure to answer is
|
|
43
180
|
# reported as the probe failing, never as a pass.
|
|
44
181
|
def unsupported_reason(ruby = RbConfig.ruby)
|
|
182
|
+
verdict(ruby).first
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# `[reason, cacheable]` — {unsupported_reason}'s answer, and whether it is a property of the Ruby
|
|
186
|
+
# build rather than of this one attempt.
|
|
187
|
+
def verdict(ruby)
|
|
45
188
|
output, status = run(ruby)
|
|
46
|
-
return "the Ruby::Box probe could not run #{ruby}" if status.nil?
|
|
47
|
-
return nil if status.success? && output == EXPECTED_OUTPUT
|
|
48
|
-
return "Ruby::Box is not available in Ruby #{RUBY_VERSION}" if status.exitstatus == 2
|
|
49
|
-
return "the Ruby::Box probe failed (#{status})" unless status.signaled?
|
|
189
|
+
return ["the Ruby::Box probe could not run #{ruby}", false] if status.nil?
|
|
190
|
+
return [nil, true] if status.success? && output == EXPECTED_OUTPUT
|
|
191
|
+
return ["Ruby::Box is not available in Ruby #{RUBY_VERSION}", true] if status.exitstatus == 2
|
|
192
|
+
return ["the Ruby::Box probe failed (#{status})", false] unless status.signaled?
|
|
193
|
+
|
|
194
|
+
["Ruby #{RUBY_VERSION} crashes running Ractor-shareable procs inside Ruby::Box " \
|
|
195
|
+
"(Ruby Bug #22260; a Ruby carrying its fix is needed)", crash_signal?(status.termsig)]
|
|
196
|
+
end
|
|
50
197
|
|
|
51
|
-
|
|
52
|
-
|
|
198
|
+
def crash_signal?(signo)
|
|
199
|
+
CRASH_SIGNALS.any? { |name| Signal.list[name] == signo }
|
|
53
200
|
end
|
|
54
201
|
|
|
55
202
|
# `RUBYOPT` is cleared so a `-r` of the caller's (bootsnap, a debugger, a coverage hook) cannot fail
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "environment"
|
|
4
|
+
require_relative "inference/project_patched_scanner"
|
|
4
5
|
require_relative "plugin"
|
|
5
6
|
require_relative "plugin/loader"
|
|
6
7
|
require_relative "plugin/services"
|
|
@@ -55,9 +56,15 @@ module Rigor
|
|
|
55
56
|
# `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
|
|
56
57
|
# @param feature_sources — path => source text the required-feature scan reads in place of that file (an
|
|
57
58
|
# editor buffer bound with `--tmp-file` / `--instead-of`).
|
|
58
|
-
|
|
59
|
+
# @param project_patched_methods — the `pre_eval:` registry. Defaults to the configuration's own (see
|
|
60
|
+
# {.project_patched_methods}), so every probe command (`type-scan`, `trace`, `annotate`, `sig-gen`) types a
|
|
61
|
+
# patched method as `rigor check` does (issue #1895); `type-of` passes its buffer-aware one. An explicit
|
|
62
|
+
# nil keeps the unpatched universe.
|
|
63
|
+
def build(configuration:, source_files:, feature_sources: {},
|
|
64
|
+
project_patched_methods: ProjectEnvironment.project_patched_methods(configuration))
|
|
59
65
|
features = required_features(configuration, source_files, sources: feature_sources)
|
|
60
66
|
Environment.for_project(
|
|
67
|
+
project_patched_methods: project_patched_methods,
|
|
61
68
|
libraries: configuration.libraries,
|
|
62
69
|
signature_paths: configuration.signature_paths,
|
|
63
70
|
plugin_registry: load_plugin_registry(configuration),
|
|
@@ -69,6 +76,21 @@ module Rigor
|
|
|
69
76
|
bare(configuration, features)
|
|
70
77
|
end
|
|
71
78
|
|
|
79
|
+
# The patched-method registry `rigor check` types against for the project's `pre_eval:` files. It depends
|
|
80
|
+
# on those files alone, so it is built by the same {Inference::ProjectPatchedScanner} the Runner's project
|
|
81
|
+
# pre-pass calls, over the same resolved `configuration.pre_eval` entries (glob-expanded at config load,
|
|
82
|
+
# missing files skipped), without the pre-pass's plugin load / `#prepare` / dependency-source work. An
|
|
83
|
+
# editor `buffer` is honoured, so a `--tmp-file` edit of a pre_eval file is read in flight. Nil when the
|
|
84
|
+
# project lists no `pre_eval:` entry, so a project without one stays byte-identical. Fail-soft.
|
|
85
|
+
def project_patched_methods(configuration, buffer: nil)
|
|
86
|
+
return nil if configuration.pre_eval.empty?
|
|
87
|
+
|
|
88
|
+
existing = configuration.pre_eval.select { |path| File.file?(path) }
|
|
89
|
+
Inference::ProjectPatchedScanner.scan(existing, buffer: buffer).registry
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
72
94
|
# Issue #1700 — the {Environment::RequiredFeatures} the environment gates on: the configured paths plus
|
|
73
95
|
# `extra_files` (the file a probe inspects, which may lie outside them), the same set `rigor check` scans
|
|
74
96
|
# whichever files it is given, so a position types as `rigor check` types it.
|
|
@@ -84,7 +84,8 @@ module Rigor
|
|
|
84
84
|
|
|
85
85
|
# Human-readable reasons the caller prints. Each names something the operator can act on.
|
|
86
86
|
NO_SNAPSHOT = "no reusable `rigor check --incremental` snapshot"
|
|
87
|
-
OPAQUE_PLUGIN = "a plugin
|
|
87
|
+
OPAQUE_PLUGIN = "a plugin is opaque to the incremental fingerprint " \
|
|
88
|
+
"(no fingerprint surface, or a value it cannot digest)"
|
|
88
89
|
UNDIGESTIBLE = "the project-scan tables could not be digested"
|
|
89
90
|
UNIDENTIFIED_ENGINE = "the engine's own source tree could not be digested"
|
|
90
91
|
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -251,11 +251,11 @@ module Rigor
|
|
|
251
251
|
# class in a project that publishes nothing) allocates none.
|
|
252
252
|
EMPTY_PUBLISHED_CONSTANT_IVARS = Set.new.freeze
|
|
253
253
|
# The index `||=` sites of a repeating block body whose slot an earlier run of the body may have filled,
|
|
254
|
-
# keyed by node identity and laid by a block-return pass
|
|
255
|
-
# ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which).
|
|
256
|
-
# (`StatementEvaluator#index_compound_write_value`) is withheld at such a site.
|
|
257
|
-
# site, so no rebind or narrowing of a variable drops it, and a join keeps a site
|
|
258
|
-
# mark only ever withholds that reading, whose answer is the narrower one.
|
|
254
|
+
# keyed by node identity and laid at the body's entry by a block-return pass or by `StatementEvaluator`'s
|
|
255
|
+
# statement and write-back passes ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which).
|
|
256
|
+
# The memoizing `||=` reading (`StatementEvaluator#index_compound_write_value`) is withheld at such a site.
|
|
257
|
+
# The mark belongs to the site, so no rebind or narrowing of a variable drops it, and a join keeps a site
|
|
258
|
+
# either arm holds: the mark only ever withholds that reading, whose answer is the narrower one.
|
|
259
259
|
EMPTY_REPEATED_OR_WRITES = {}.compare_by_identity.freeze
|
|
260
260
|
# Issue #1429 — the narrowing a guard leaves on a constant reference (`STDOUT.is_a?(StringIO)`, `if CONFIG`),
|
|
261
261
|
# keyed by the reference's spelling ({Inference::Narrowing.constant_key}: `"STDOUT"`, `"::Foo::BAR"`). A constant
|
|
@@ -457,6 +457,13 @@ module Rigor
|
|
|
457
457
|
@locals[name.to_sym]
|
|
458
458
|
end
|
|
459
459
|
|
|
460
|
+
# Issue #1840 — replaces the joined bindings of locals a branch merge only narrowed with their pre-branch
|
|
461
|
+
# binding ({Inference::UntypedRejoin}). Unlike {#with_local} this is not a write: the value is the one the
|
|
462
|
+
# branches started from, so every fact, narrowing and mark the join kept about it still holds.
|
|
463
|
+
def with_rejoined_locals(bindings)
|
|
464
|
+
rebuild(locals: @locals.merge(bindings).freeze)
|
|
465
|
+
end
|
|
466
|
+
|
|
460
467
|
def with_local(name, type)
|
|
461
468
|
bind_local(name, type, keep_marks: false)
|
|
462
469
|
end
|
|
@@ -869,13 +876,18 @@ module Rigor
|
|
|
869
876
|
STDOUT_ALIASES = { :$stdout => :$>, :$> => :$stdout }.freeze
|
|
870
877
|
private_constant :STDOUT_ALIASES
|
|
871
878
|
|
|
872
|
-
# Issue #1429 — {#forget_guard_narrowings} for the one name `[kind, name]`.
|
|
879
|
+
# Issue #1429 — {#forget_guard_narrowings} for the one name `[kind, name]`. Issue #1596 — an `:ivar` kind too,
|
|
880
|
+
# for a self-call proven to write only some instance variables.
|
|
873
881
|
def forget_guard_narrowing_of(kind, name)
|
|
874
882
|
key = [kind, name]
|
|
875
883
|
pre_guard = @guard_records[key]
|
|
876
884
|
return self if pre_guard.nil?
|
|
877
885
|
|
|
878
|
-
if kind == :
|
|
886
|
+
if kind == :ivar
|
|
887
|
+
current = @ivars[name]
|
|
888
|
+
ivars = current ? @ivars.merge(name => Type::Combinator.union(pre_guard, current)).freeze : @ivars
|
|
889
|
+
rebuild(ivars: ivars, guard_records: @guard_records.except(key).freeze)
|
|
890
|
+
elsif kind == :global
|
|
879
891
|
current = @globals[name]
|
|
880
892
|
globals = current ? @globals.merge(name => Type::Combinator.union(pre_guard, current)).freeze : @globals
|
|
881
893
|
rebuild(globals: globals, guard_records: @guard_records.except(key).freeze)
|
|
@@ -1055,20 +1067,22 @@ module Rigor
|
|
|
1055
1067
|
@repeated_or_writes.key?(node)
|
|
1056
1068
|
end
|
|
1057
1069
|
|
|
1058
|
-
# Regex match-data globals (`$~`, `$&`, `$1..$
|
|
1070
|
+
# Regex match-data globals (`$~`, `$&`, `$1..$N`, the pre/post-match and last-paren back-references). Narrowed
|
|
1059
1071
|
# on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`). They live in
|
|
1060
1072
|
# the method frame's special-variable slot, which the method's blocks and closures share (issue #1358), so a
|
|
1061
1073
|
# later call that may run a match in this frame — a match-capable call, a block that may match, a call once
|
|
1062
1074
|
# the frame has made a closure that may — rebinds every one of them, and `eval_call` forgets the narrowed
|
|
1063
1075
|
# facts here. Always safe — only drops facts, so a subsequent read falls back to the default `String | nil`.
|
|
1064
1076
|
# Program-level `$GLOBAL = ...` seeds use other names and are untouched.
|
|
1065
|
-
|
|
1066
|
-
|
|
1077
|
+
# Issue #1384 — the numbered groups are matched by name, not listed: a regex may carry more than nine captures, and
|
|
1078
|
+
# `$10` and above are the same slot as `$1`..`$9`, so forgetting must clear them too.
|
|
1079
|
+
MATCH_DATA_GLOBAL_NAME = /\A\$(?:~|&|`|'|\+|[1-9]\d*)\z/
|
|
1080
|
+
private_constant :MATCH_DATA_GLOBAL_NAME
|
|
1067
1081
|
|
|
1068
1082
|
def forget_match_globals
|
|
1069
1083
|
return self unless match_globals_bound?
|
|
1070
1084
|
|
|
1071
|
-
rebuild(globals: @globals.
|
|
1085
|
+
rebuild(globals: @globals.reject { |name, _| MATCH_DATA_GLOBAL_NAME.match?(name.name) }.freeze)
|
|
1072
1086
|
end
|
|
1073
1087
|
|
|
1074
1088
|
# Issue #1361 — this scope with every bound match-data global rebound to `Dynamic[top]`, and an unbound one left
|
|
@@ -1079,14 +1093,16 @@ module Rigor
|
|
|
1079
1093
|
return self unless match_globals_bound?
|
|
1080
1094
|
|
|
1081
1095
|
untyped = Type::Combinator.untyped
|
|
1082
|
-
rebound =
|
|
1096
|
+
rebound = @globals.each_with_object({}) do |(name, _), acc|
|
|
1097
|
+
acc[name] = untyped if MATCH_DATA_GLOBAL_NAME.match?(name.name)
|
|
1098
|
+
end
|
|
1083
1099
|
rebuild(globals: @globals.merge(rebound).freeze)
|
|
1084
1100
|
end
|
|
1085
1101
|
|
|
1086
1102
|
# True when any match-data global holds a binding, which a match edge or {#untyped_match_globals} makes: the only
|
|
1087
1103
|
# state {#forget_match_globals} can drop, and so the gate on every scan that decides whether to.
|
|
1088
1104
|
def match_globals_bound?
|
|
1089
|
-
!@globals.empty? &&
|
|
1105
|
+
!@globals.empty? && @globals.any? { |name, _| MATCH_DATA_GLOBAL_NAME.match?(name.name) }
|
|
1090
1106
|
end
|
|
1091
1107
|
|
|
1092
1108
|
# Issue #1359 — the last line read, `$_`, lives in the same frame slot as the match globals, and so on the same
|
|
@@ -1910,10 +1926,10 @@ module Rigor
|
|
|
1910
1926
|
private_constant :EMPTY_PARAMETER_ENVELOPES
|
|
1911
1927
|
|
|
1912
1928
|
# v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
|
|
1913
|
-
# `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from
|
|
1914
|
-
# `private` / `
|
|
1915
|
-
#
|
|
1916
|
-
# surface a diagnostic.
|
|
1929
|
+
# `ScopeIndexer` for every `def` and `attr_*` member it sees inside a class body, with the visibility taken from
|
|
1930
|
+
# a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
|
|
1931
|
+
# modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
|
|
1932
|
+
# `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method surface a diagnostic.
|
|
1917
1933
|
def discovered_method_visibility(class_name, method_name)
|
|
1918
1934
|
table = @discovery.discovered_method_visibilities[class_name.to_s]
|
|
1919
1935
|
return nil unless table
|