rigortype 0.1.3 → 0.1.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 +125 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +114 -3
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +2 -2
- data/lib/rigor/environment.rb +35 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +146 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +30 -9
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +52 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
require_relative "../configuration"
|
|
6
|
+
require_relative "../sig_gen"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
class CLI
|
|
10
|
+
# Executes the `rigor sig-gen` command — ADR-14 slices 1–3.
|
|
11
|
+
#
|
|
12
|
+
# Walks the given paths (or `configuration.paths` when none
|
|
13
|
+
# are supplied), classifies every reachable instance method
|
|
14
|
+
# via {Rigor::SigGen::Generator}, and either prints the
|
|
15
|
+
# resulting RBS skeletons / unified-style diffs (`--print`,
|
|
16
|
+
# `--diff`; slice 1) or writes them to the project signature
|
|
17
|
+
# tree via {Rigor::SigGen::Writer} (`--write`; slice 2).
|
|
18
|
+
#
|
|
19
|
+
# `--write` follows the established Ruby community
|
|
20
|
+
# convention: `lib/foo/bar.rb` → `sig/foo/bar.rbs`. New
|
|
21
|
+
# methods are inserted into the matching class declaration
|
|
22
|
+
# just before its closing `end`; new classes are appended
|
|
23
|
+
# to the file; non-existent target files are created. User-
|
|
24
|
+
# authored declarations are NEVER replaced unless
|
|
25
|
+
# `--overwrite` is set AND the candidate is a
|
|
26
|
+
# `tighter-return`.
|
|
27
|
+
#
|
|
28
|
+
# Parameter policy defaults to `untyped`. `--params=observed`
|
|
29
|
+
# (slice 3) opts in to caller-side observation harvesting:
|
|
30
|
+
# the `ObservationCollector` walks `--observe=PATH...`
|
|
31
|
+
# (default `spec/` when no flag is given AND a `spec/`
|
|
32
|
+
# directory exists), unions per-position arg types, and the
|
|
33
|
+
# generator emits the union per ADR-5 clause 2.
|
|
34
|
+
# `--params=observed-strict` stays reserved-but-inert until
|
|
35
|
+
# the capability-role catalog ships (rejected with a usage
|
|
36
|
+
# error so the surface stays stable).
|
|
37
|
+
class SigGenCommand
|
|
38
|
+
USAGE = "Usage: rigor sig-gen [options] [paths]"
|
|
39
|
+
|
|
40
|
+
VALID_MODES = %w[print diff write].freeze
|
|
41
|
+
VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
|
|
42
|
+
VALID_FORMATS = %w[text json].freeze
|
|
43
|
+
|
|
44
|
+
def initialize(argv:, out:, err:)
|
|
45
|
+
@argv = argv
|
|
46
|
+
@out = out
|
|
47
|
+
@err = err
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Integer] CLI exit status.
|
|
51
|
+
def run
|
|
52
|
+
options = parse_options
|
|
53
|
+
return CLI::EXIT_USAGE if options.nil?
|
|
54
|
+
|
|
55
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
56
|
+
paths = @argv.empty? ? configuration.paths : @argv
|
|
57
|
+
|
|
58
|
+
observations = collect_observations(configuration, options)
|
|
59
|
+
candidates = SigGen::Generator.new(configuration: configuration, paths: paths,
|
|
60
|
+
observations: observations,
|
|
61
|
+
include_private: options.fetch(:include_private)).run
|
|
62
|
+
mode = options.fetch(:mode).to_sym
|
|
63
|
+
|
|
64
|
+
if mode == :write
|
|
65
|
+
dispatch_write(candidates, configuration, options)
|
|
66
|
+
else
|
|
67
|
+
dispatch_print_or_diff(candidates, mode, options)
|
|
68
|
+
end
|
|
69
|
+
0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def dispatch_print_or_diff(candidates, mode, options)
|
|
75
|
+
SigGen::Renderer.new(out: @out).render(
|
|
76
|
+
candidates: candidates,
|
|
77
|
+
mode: mode,
|
|
78
|
+
format: options.fetch(:format),
|
|
79
|
+
selection: options.fetch(:selection)
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def dispatch_write(candidates, configuration, options)
|
|
84
|
+
layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
|
|
85
|
+
path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
|
|
86
|
+
writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
|
|
87
|
+
|
|
88
|
+
results = writer.write_all(candidates)
|
|
89
|
+
|
|
90
|
+
SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Slice 3 — collect call-site argument observations when
|
|
94
|
+
# `--params=observed` is set. When `--observe=PATH` is
|
|
95
|
+
# not specified, default to `spec/` (skipped silently
|
|
96
|
+
# when the directory is absent).
|
|
97
|
+
def collect_observations(configuration, options)
|
|
98
|
+
return {} if options.fetch(:params) != "observed"
|
|
99
|
+
|
|
100
|
+
observe_paths = options.fetch(:observe)
|
|
101
|
+
observe_paths = ["spec"] if observe_paths.empty? && File.directory?("spec")
|
|
102
|
+
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_options
|
|
106
|
+
options = {
|
|
107
|
+
mode: "print",
|
|
108
|
+
format: "text",
|
|
109
|
+
params: "untyped",
|
|
110
|
+
selection: [],
|
|
111
|
+
overwrite: false,
|
|
112
|
+
observe: [],
|
|
113
|
+
include_private: false,
|
|
114
|
+
config: nil
|
|
115
|
+
}
|
|
116
|
+
build_option_parser(options).parse!(@argv)
|
|
117
|
+
|
|
118
|
+
message = validation_error(options)
|
|
119
|
+
return options if message.nil?
|
|
120
|
+
|
|
121
|
+
@err.puts("sig-gen: #{message}")
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_option_parser(options) # rubocop:disable Metrics/AbcSize
|
|
126
|
+
OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
127
|
+
opts.banner = USAGE
|
|
128
|
+
opts.on("--print", "Write RBS skeletons to stdout (default)") { options[:mode] = "print" }
|
|
129
|
+
opts.on("--diff", "Write a unified diff against existing RBS") { options[:mode] = "diff" }
|
|
130
|
+
opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options[:mode] = "write" }
|
|
131
|
+
opts.on("--overwrite", "Allow tighter-return updates to replace user-authored RBS") do
|
|
132
|
+
options[:overwrite] = true
|
|
133
|
+
end
|
|
134
|
+
opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
|
|
135
|
+
options[:include_private] = true
|
|
136
|
+
end
|
|
137
|
+
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
138
|
+
opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
|
|
139
|
+
options[:params] = value
|
|
140
|
+
end
|
|
141
|
+
opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable)") do |value|
|
|
142
|
+
options[:observe] << value
|
|
143
|
+
end
|
|
144
|
+
opts.on("--new-files", "Emit only new-file classifications") do
|
|
145
|
+
options[:selection] << SigGen::Classification::NEW_FILE
|
|
146
|
+
end
|
|
147
|
+
opts.on("--new-methods", "Emit only new-method classifications") do
|
|
148
|
+
options[:selection] << SigGen::Classification::NEW_METHOD
|
|
149
|
+
end
|
|
150
|
+
opts.on("--tighter-returns", "Emit only tighter-return classifications") do
|
|
151
|
+
options[:selection] << SigGen::Classification::TIGHTER_RETURN
|
|
152
|
+
end
|
|
153
|
+
opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def validation_error(options)
|
|
158
|
+
mode = options.fetch(:mode)
|
|
159
|
+
format = options.fetch(:format)
|
|
160
|
+
params = options.fetch(:params)
|
|
161
|
+
|
|
162
|
+
return "--print, --diff, and --write are mutually exclusive flags; pick one" unless VALID_MODES.include?(mode)
|
|
163
|
+
return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
|
|
164
|
+
return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
|
|
165
|
+
if params == "observed-strict"
|
|
166
|
+
return "--params=observed-strict is reserved until the capability-role catalog ships"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -10,7 +10,7 @@ module Rigor
|
|
|
10
10
|
# branches share a single source of truth (the `Report` value object) so
|
|
11
11
|
# the two formats stay in lockstep; that pairing is why this class is a
|
|
12
12
|
# bit longer than the default class-length budget.
|
|
13
|
-
class TypeScanRenderer
|
|
13
|
+
class TypeScanRenderer
|
|
14
14
|
def initialize(out:)
|
|
15
15
|
@out = out
|
|
16
16
|
end
|
|
@@ -5,14 +5,14 @@ module Rigor
|
|
|
5
5
|
# Aggregated report assembled by `TypeScanCommand` and consumed by
|
|
6
6
|
# `TypeScanRenderer`. The struct holds per-file paths, accumulated
|
|
7
7
|
# per-class counts, located fallback events, and any parse errors.
|
|
8
|
-
Report
|
|
8
|
+
class Report < Data.define(
|
|
9
9
|
:files,
|
|
10
10
|
:parse_errors,
|
|
11
11
|
:visits,
|
|
12
12
|
:unrecognized,
|
|
13
13
|
:events,
|
|
14
14
|
:options
|
|
15
|
-
)
|
|
15
|
+
)
|
|
16
16
|
def visited_count
|
|
17
17
|
visits.values.sum
|
|
18
18
|
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -24,7 +24,8 @@ module Rigor
|
|
|
24
24
|
"type-of" => :run_type_of,
|
|
25
25
|
"type-scan" => :run_type_scan,
|
|
26
26
|
"explain" => :run_explain,
|
|
27
|
-
"diff" => :run_diff
|
|
27
|
+
"diff" => :run_diff,
|
|
28
|
+
"sig-gen" => :run_sig_gen
|
|
28
29
|
}.freeze
|
|
29
30
|
|
|
30
31
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -279,6 +280,12 @@ module Rigor
|
|
|
279
280
|
DiffCommand.new(argv: @argv, out: @out, err: @err).run
|
|
280
281
|
end
|
|
281
282
|
|
|
283
|
+
def run_sig_gen
|
|
284
|
+
require_relative "cli/sig_gen_command"
|
|
285
|
+
|
|
286
|
+
SigGenCommand.new(argv: @argv, out: @out, err: @err).run
|
|
287
|
+
end
|
|
288
|
+
|
|
282
289
|
def write_result(result, format)
|
|
283
290
|
case format
|
|
284
291
|
when "json"
|
|
@@ -318,6 +325,7 @@ module Rigor
|
|
|
318
325
|
type-scan Report Scope#type_of coverage across PATHs
|
|
319
326
|
explain Print the description of one or all CheckRules
|
|
320
327
|
diff Compare current diagnostics to a saved baseline JSON
|
|
328
|
+
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
321
329
|
version Print the Rigor version
|
|
322
330
|
help Print this help
|
|
323
331
|
HELP
|
|
@@ -13,7 +13,7 @@ module Rigor
|
|
|
13
13
|
# is read, but no analyzer machinery consumes it yet. Slice 2
|
|
14
14
|
# wires `Analysis::DependencySourceInference` against this
|
|
15
15
|
# value object.
|
|
16
|
-
class Dependencies
|
|
16
|
+
class Dependencies
|
|
17
17
|
# Walking modes per
|
|
18
18
|
# [ADR-10 § "Decision"](../../../docs/adr/10-dependency-source-inference.md#decision).
|
|
19
19
|
VALID_MODES = %i[disabled when_missing full].freeze
|
|
@@ -61,7 +61,7 @@ module Rigor
|
|
|
61
61
|
# walk time); `mode:` is one of {VALID_MODES}; `roots:` is
|
|
62
62
|
# the list of subdirectories within the gem's installation
|
|
63
63
|
# directory to walk (defaults to `["lib"]`).
|
|
64
|
-
Entry
|
|
64
|
+
class Entry < Data.define(:gem, :mode, :roots)
|
|
65
65
|
def disabled? = mode == :disabled
|
|
66
66
|
def when_missing? = mode == :when_missing
|
|
67
67
|
def full? = mode == :full
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -214,7 +214,7 @@ module Rigor
|
|
|
214
214
|
private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
|
|
215
215
|
:merge_value, :merge_dependencies_hash
|
|
216
216
|
|
|
217
|
-
# rubocop:disable Metrics/AbcSize
|
|
217
|
+
# rubocop:disable Metrics/AbcSize
|
|
218
218
|
def initialize(data = DEFAULTS)
|
|
219
219
|
cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
|
|
220
220
|
plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
|
|
@@ -247,7 +247,7 @@ module Rigor
|
|
|
247
247
|
data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
|
|
248
248
|
)
|
|
249
249
|
end
|
|
250
|
-
# rubocop:enable Metrics/AbcSize
|
|
250
|
+
# rubocop:enable Metrics/AbcSize
|
|
251
251
|
|
|
252
252
|
def to_h
|
|
253
253
|
{
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "environment/class_registry"
|
|
4
4
|
require_relative "environment/rbs_loader"
|
|
5
|
+
require_relative "type_node/name_scope"
|
|
6
|
+
require_relative "type_node/resolver_chain"
|
|
5
7
|
|
|
6
8
|
module Rigor
|
|
7
9
|
# The engine's view of the type universe outside the current scope.
|
|
@@ -41,7 +43,8 @@ module Rigor
|
|
|
41
43
|
prism rbs
|
|
42
44
|
].freeze
|
|
43
45
|
|
|
44
|
-
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index
|
|
46
|
+
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
47
|
+
:rbs_extended_reporter, :boundary_cross_reporter, :name_scope
|
|
45
48
|
|
|
46
49
|
# @param class_registry [Rigor::Environment::ClassRegistry]
|
|
47
50
|
# @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
|
|
@@ -63,11 +66,15 @@ module Rigor
|
|
|
63
66
|
# When nil (the default), no dep-source contribution
|
|
64
67
|
# participates and the dispatcher tier is a no-op.
|
|
65
68
|
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
|
|
66
|
-
plugin_registry: nil, dependency_source_index: nil
|
|
69
|
+
plugin_registry: nil, dependency_source_index: nil,
|
|
70
|
+
rbs_extended_reporter: nil, boundary_cross_reporter: nil)
|
|
67
71
|
@class_registry = class_registry
|
|
68
72
|
@rbs_loader = rbs_loader
|
|
69
73
|
@plugin_registry = plugin_registry
|
|
70
74
|
@dependency_source_index = dependency_source_index
|
|
75
|
+
@rbs_extended_reporter = rbs_extended_reporter
|
|
76
|
+
@boundary_cross_reporter = boundary_cross_reporter
|
|
77
|
+
@name_scope = build_name_scope
|
|
71
78
|
freeze
|
|
72
79
|
end
|
|
73
80
|
|
|
@@ -98,7 +105,8 @@ module Rigor
|
|
|
98
105
|
# default) to skip caching for this environment.
|
|
99
106
|
# @return [Rigor::Environment]
|
|
100
107
|
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/ParameterLists
|
|
101
|
-
plugin_registry: nil, dependency_source_index: nil
|
|
108
|
+
plugin_registry: nil, dependency_source_index: nil,
|
|
109
|
+
rbs_extended_reporter: nil, boundary_cross_reporter: nil)
|
|
102
110
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
103
111
|
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
104
112
|
loader = RbsLoader.new(
|
|
@@ -109,7 +117,9 @@ module Rigor
|
|
|
109
117
|
new(
|
|
110
118
|
rbs_loader: loader,
|
|
111
119
|
plugin_registry: plugin_registry,
|
|
112
|
-
dependency_source_index: dependency_source_index
|
|
120
|
+
dependency_source_index: dependency_source_index,
|
|
121
|
+
rbs_extended_reporter: rbs_extended_reporter,
|
|
122
|
+
boundary_cross_reporter: boundary_cross_reporter
|
|
113
123
|
)
|
|
114
124
|
end
|
|
115
125
|
|
|
@@ -204,5 +214,26 @@ module Rigor
|
|
|
204
214
|
def normalize_class_name(name)
|
|
205
215
|
name.to_s.delete_prefix("::")
|
|
206
216
|
end
|
|
217
|
+
|
|
218
|
+
# ADR-13 slice 3b — composes the per-run plugin-supplied
|
|
219
|
+
# {Rigor::TypeNode::ResolverChain} into a single
|
|
220
|
+
# {Rigor::TypeNode::NameScope} that the RBS::Extended
|
|
221
|
+
# directive parser threads down to the
|
|
222
|
+
# {Rigor::Builtins::ImportedRefinements::Resolver}. Returns
|
|
223
|
+
# `nil` when no plugin contributes a type-node resolver so
|
|
224
|
+
# the parser short-circuits the chain consultation and
|
|
225
|
+
# behaves bit-for-bit like the v0.1.0 → v0.1.3 default.
|
|
226
|
+
def build_name_scope
|
|
227
|
+
return nil if @plugin_registry.nil? || @plugin_registry.empty?
|
|
228
|
+
|
|
229
|
+
resolvers = @plugin_registry.type_node_resolvers
|
|
230
|
+
return nil if resolvers.empty?
|
|
231
|
+
|
|
232
|
+
TypeNode::NameScope.new(
|
|
233
|
+
resolver: TypeNode::ResolverChain.new(resolvers),
|
|
234
|
+
class_context: nil,
|
|
235
|
+
type_alias_table: {}
|
|
236
|
+
)
|
|
237
|
+
end
|
|
207
238
|
end
|
|
208
239
|
end
|
|
@@ -28,8 +28,8 @@ module Rigor
|
|
|
28
28
|
lower_tier_contradiction
|
|
29
29
|
].freeze
|
|
30
30
|
|
|
31
|
-
Conflict
|
|
32
|
-
def initialize(target:, edge:, kind:, reason:, provenances:, message:)
|
|
31
|
+
class Conflict < Data.define(:target, :edge, :kind, :reason, :provenances, :message)
|
|
32
|
+
def initialize(target:, edge:, kind:, reason:, provenances:, message:)
|
|
33
33
|
unless CONFLICT_VALID_REASONS.include?(reason)
|
|
34
34
|
raise ArgumentError,
|
|
35
35
|
"FlowContribution::Conflict reason must be one of " \
|
|
@@ -28,7 +28,7 @@ module Rigor
|
|
|
28
28
|
role
|
|
29
29
|
].freeze
|
|
30
30
|
|
|
31
|
-
Element
|
|
31
|
+
class Element < Data.define(:target, :edge, :kind, :payload, :provenance)
|
|
32
32
|
def initialize(target:, edge:, kind:, payload:, provenance:)
|
|
33
33
|
unless ELEMENT_VALID_EDGES.include?(edge)
|
|
34
34
|
raise ArgumentError,
|
|
@@ -55,7 +55,7 @@ module Rigor
|
|
|
55
55
|
# land in the same merge bucket.
|
|
56
56
|
FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
|
|
57
57
|
|
|
58
|
-
Fact
|
|
58
|
+
class Fact < Data.define(:target_kind, :target_name, :type, :negative)
|
|
59
59
|
def initialize(target_kind:, target_name:, type:, negative: false)
|
|
60
60
|
unless FACT_VALID_TARGET_KINDS.include?(target_kind)
|
|
61
61
|
raise ArgumentError,
|
|
@@ -42,7 +42,7 @@ module Rigor
|
|
|
42
42
|
!@conflicts.empty?
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def empty?
|
|
45
|
+
def empty?
|
|
46
46
|
@return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
|
|
47
47
|
@post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
|
|
48
48
|
@exceptional.nil? && @role_conformance.empty?
|
|
@@ -55,7 +55,7 @@ module Rigor
|
|
|
55
55
|
# In every conflict case the result keeps the higher-tier value
|
|
56
56
|
# for that slot, records a {Conflict} with both provenances, and
|
|
57
57
|
# continues processing the remaining slots / contributions.
|
|
58
|
-
module Merger
|
|
58
|
+
module Merger
|
|
59
59
|
AUTHORITY_TIERS = {
|
|
60
60
|
builtin: 0,
|
|
61
61
|
rbs_extended: 1,
|
|
@@ -112,7 +112,7 @@ module Rigor
|
|
|
112
112
|
fold_role_conformance(state, contribution)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def fold_return_type(state, contribution, tier)
|
|
115
|
+
def fold_return_type(state, contribution, tier)
|
|
116
116
|
incoming = contribution.return_type
|
|
117
117
|
return if incoming.nil?
|
|
118
118
|
|
|
@@ -202,7 +202,7 @@ module Rigor
|
|
|
202
202
|
end
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
-
def build_conflict(target:, edge:, kind:, reason:, provenances:, message:)
|
|
205
|
+
def build_conflict(target:, edge:, kind:, reason:, provenances:, message:)
|
|
206
206
|
Conflict.new(target: target, edge: edge, kind: kind, reason: reason,
|
|
207
207
|
provenances: provenances, message: message)
|
|
208
208
|
end
|
|
@@ -32,7 +32,7 @@ module Rigor
|
|
|
32
32
|
# `descriptor` is the {Rigor::Cache::Descriptor} this
|
|
33
33
|
# contribution attaches to (or `nil` when the contribution does
|
|
34
34
|
# not need its own cache slice).
|
|
35
|
-
Provenance
|
|
35
|
+
class Provenance < Data.define(:source_family, :plugin_id, :node, :descriptor)
|
|
36
36
|
def self.builtin
|
|
37
37
|
new(source_family: :builtin, plugin_id: nil, node: nil, descriptor: nil)
|
|
38
38
|
end
|
|
@@ -122,7 +122,7 @@ module Rigor
|
|
|
122
122
|
# | role_conformance | normal | role | (per-role target) |
|
|
123
123
|
#
|
|
124
124
|
# @return [Array<Element>]
|
|
125
|
-
def to_element_list # rubocop:disable Metrics/AbcSize
|
|
125
|
+
def to_element_list # rubocop:disable Metrics/AbcSize
|
|
126
126
|
elements = []
|
|
127
127
|
elements << element_for(:return, :normal, :return_type, return_type) unless return_type.nil?
|
|
128
128
|
Array(truthy_facts).each { |fact| elements << element_for(fact_target(fact), :truthy, :truthy_fact, fact) }
|
|
@@ -45,7 +45,6 @@ module Rigor
|
|
|
45
45
|
# scope MUST NOT observe them and the binder leaves them unbound.
|
|
46
46
|
#
|
|
47
47
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
48
|
-
# rubocop:disable Metrics/ClassLength
|
|
49
48
|
class BlockParameterBinder
|
|
50
49
|
# @param expected_param_types [Array<Rigor::Type>] positional block
|
|
51
50
|
# parameter types in order. Indices the binder cannot fill from
|
|
@@ -208,6 +207,5 @@ module Rigor
|
|
|
208
207
|
@expected_param_types[index] || Type::Combinator.untyped
|
|
209
208
|
end
|
|
210
209
|
end
|
|
211
|
-
# rubocop:enable Metrics/ClassLength
|
|
212
210
|
end
|
|
213
211
|
end
|
|
@@ -24,7 +24,7 @@ module Rigor
|
|
|
24
24
|
# hot inference path: it allocates a tracer per visited node and discards
|
|
25
25
|
# the inferred type values.
|
|
26
26
|
class CoverageScanner
|
|
27
|
-
Result
|
|
27
|
+
class Result < Data.define(:visits, :unrecognized, :events)
|
|
28
28
|
# @return [Integer] sum of all visits across node classes.
|
|
29
29
|
def visited_count
|
|
30
30
|
visits.values.sum
|
|
@@ -111,7 +111,21 @@ module Rigor
|
|
|
111
111
|
Prism::IndexOrWriteNode => :type_of_assignment_write,
|
|
112
112
|
Prism::IndexAndWriteNode => :type_of_assignment_write,
|
|
113
113
|
Prism::MultiWriteNode => :type_of_assignment_write,
|
|
114
|
+
# LHS-only target nodes (destructuring assignment, pattern matching,
|
|
115
|
+
# `for x in xs`, block parameter `|a, (b, c)|`). They have no value
|
|
116
|
+
# to extract — the type-of pass acknowledges the node class so the
|
|
117
|
+
# coverage scanner stops flagging it; binding the inner names back
|
|
118
|
+
# into the scope is the StatementEvaluator / MultiTargetBinder /
|
|
119
|
+
# BlockParameterBinder side's concern.
|
|
114
120
|
Prism::LocalVariableTargetNode => :type_of_non_value,
|
|
121
|
+
Prism::MultiTargetNode => :type_of_non_value,
|
|
122
|
+
Prism::InstanceVariableTargetNode => :type_of_non_value,
|
|
123
|
+
Prism::ClassVariableTargetNode => :type_of_non_value,
|
|
124
|
+
Prism::GlobalVariableTargetNode => :type_of_non_value,
|
|
125
|
+
Prism::ConstantTargetNode => :type_of_non_value,
|
|
126
|
+
Prism::ConstantPathTargetNode => :type_of_non_value,
|
|
127
|
+
Prism::CallTargetNode => :type_of_non_value,
|
|
128
|
+
Prism::IndexTargetNode => :type_of_non_value,
|
|
115
129
|
# Hashes and interpolation
|
|
116
130
|
Prism::HashNode => :type_of_hash,
|
|
117
131
|
Prism::KeywordHashNode => :type_of_hash,
|
|
@@ -931,7 +945,6 @@ module Rigor
|
|
|
931
945
|
# for the CallNode itself (the inner type_of calls already record
|
|
932
946
|
# their own fallbacks for unrecognised receivers/args, so the tracer
|
|
933
947
|
# captures both the immediate dispatch miss and the deeper cause).
|
|
934
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
935
948
|
def call_type_for(node)
|
|
936
949
|
receiver = call_receiver_type_for(node)
|
|
937
950
|
arg_types = call_arg_types(node)
|
|
@@ -1004,7 +1017,6 @@ module Rigor
|
|
|
1004
1017
|
|
|
1005
1018
|
fallback_for(node, family: :prism)
|
|
1006
1019
|
end
|
|
1007
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
1008
1020
|
|
|
1009
1021
|
# v0.0.2 #5 — re-types the body of a user-defined
|
|
1010
1022
|
# instance method with the call site's argument types
|
|
@@ -1160,9 +1172,20 @@ module Rigor
|
|
|
1160
1172
|
# when typing the body raises (defensive against malformed
|
|
1161
1173
|
# subtrees); the dispatcher then runs in its no-block-aware
|
|
1162
1174
|
# path.
|
|
1175
|
+
#
|
|
1176
|
+
# ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying
|
|
1177
|
+
# `&:symbol` (the Symbol#to_proc shorthand) is treated as
|
|
1178
|
+
# a block. The block's return type is computed by
|
|
1179
|
+
# dispatching `:symbol` on the expected block param type
|
|
1180
|
+
# (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics).
|
|
1181
|
+
# A precise inner dispatch produces the right return; any
|
|
1182
|
+
# failure step falls back to `Dynamic[Top]` so the
|
|
1183
|
+
# dispatcher still SEES a block — selecting the block-
|
|
1184
|
+
# bearing overload of e.g. `Hash#transform_values` over
|
|
1185
|
+
# the no-block overload that returns `Enumerator`.
|
|
1163
1186
|
def block_return_type_for(call_node, receiver_type, arg_types)
|
|
1164
|
-
|
|
1165
|
-
return nil
|
|
1187
|
+
block_arg = call_node.block
|
|
1188
|
+
return nil if block_arg.nil?
|
|
1166
1189
|
return nil if receiver_type.nil?
|
|
1167
1190
|
|
|
1168
1191
|
expected = MethodDispatcher.expected_block_param_types(
|
|
@@ -1171,13 +1194,50 @@ module Rigor
|
|
|
1171
1194
|
arg_types: arg_types,
|
|
1172
1195
|
environment: scope.environment
|
|
1173
1196
|
)
|
|
1174
|
-
|
|
1175
|
-
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
1176
|
-
type_block_body(block_node, block_scope)
|
|
1197
|
+
block_return_for(block_arg, expected)
|
|
1177
1198
|
rescue StandardError
|
|
1178
1199
|
nil
|
|
1179
1200
|
end
|
|
1180
1201
|
|
|
1202
|
+
def block_return_for(block_arg, expected)
|
|
1203
|
+
case block_arg
|
|
1204
|
+
when Prism::BlockNode
|
|
1205
|
+
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
|
|
1206
|
+
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
1207
|
+
type_block_body(block_arg, block_scope)
|
|
1208
|
+
when Prism::BlockArgumentNode
|
|
1209
|
+
symbol_block_return_type(block_arg, expected)
|
|
1210
|
+
end
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
# `&:symbol` desugars to a one-arg Proc that dispatches
|
|
1214
|
+
# `symbol` against its argument. When the param type is
|
|
1215
|
+
# known and the resulting inner dispatch is precise,
|
|
1216
|
+
# this returns the precise carrier; otherwise it
|
|
1217
|
+
# returns `Dynamic[Top]` (still non-nil) so the outer
|
|
1218
|
+
# dispatcher selects the block-bearing overload.
|
|
1219
|
+
# `&proc_local` / `&method(:foo)` and friends — anything
|
|
1220
|
+
# not a bare SymbolNode — still resolve to
|
|
1221
|
+
# `Dynamic[Top]` for the same block-presence signal.
|
|
1222
|
+
def symbol_block_return_type(block_arg, expected_param_types)
|
|
1223
|
+
expression = block_arg.expression
|
|
1224
|
+
return dynamic_top unless expression.is_a?(Prism::SymbolNode)
|
|
1225
|
+
|
|
1226
|
+
param_type = expected_param_types&.first
|
|
1227
|
+
return dynamic_top if param_type.nil?
|
|
1228
|
+
|
|
1229
|
+
result = MethodDispatcher.dispatch(
|
|
1230
|
+
receiver_type: param_type,
|
|
1231
|
+
method_name: expression.unescaped.to_sym,
|
|
1232
|
+
arg_types: [],
|
|
1233
|
+
block_type: nil,
|
|
1234
|
+
environment: scope.environment,
|
|
1235
|
+
call_node: block_arg,
|
|
1236
|
+
scope: scope
|
|
1237
|
+
)
|
|
1238
|
+
result || dynamic_top
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1181
1241
|
def type_block_body(block_node, block_scope)
|
|
1182
1242
|
body = block_node.body
|
|
1183
1243
|
return Type::Combinator.constant_of(nil) if body.nil?
|
|
@@ -1213,7 +1273,6 @@ module Rigor
|
|
|
1213
1273
|
PER_ELEMENT_RANGE_LIMIT = 8
|
|
1214
1274
|
private_constant :PER_ELEMENT_RANGE_LIMIT
|
|
1215
1275
|
|
|
1216
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
1217
1276
|
def try_per_element_block_fold(call_node, receiver_type)
|
|
1218
1277
|
return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
|
|
1219
1278
|
return nil if find_family_with_args?(call_node)
|
|
@@ -1231,7 +1290,6 @@ module Rigor
|
|
|
1231
1290
|
|
|
1232
1291
|
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
1233
1292
|
end
|
|
1234
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
1235
1293
|
|
|
1236
1294
|
# Returns the per-position element types for a finite,
|
|
1237
1295
|
# statically-known receiver shape — or nil when the
|
|
@@ -1254,7 +1312,6 @@ module Rigor
|
|
|
1254
1312
|
end
|
|
1255
1313
|
end
|
|
1256
1314
|
|
|
1257
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
1258
1315
|
def constant_range_elements(value)
|
|
1259
1316
|
return nil unless value.is_a?(Range)
|
|
1260
1317
|
return nil unless value.begin.is_a?(Integer) && value.end.is_a?(Integer)
|
|
@@ -1264,7 +1321,6 @@ module Rigor
|
|
|
1264
1321
|
|
|
1265
1322
|
value.to_a.map { |v| Type::Combinator.constant_of(v) }
|
|
1266
1323
|
end
|
|
1267
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
1268
1324
|
|
|
1269
1325
|
# `index(value)` and `find_index(value)` carry a positional
|
|
1270
1326
|
# argument and search by `==` rather than running the block.
|
|
@@ -20,7 +20,7 @@ module Rigor
|
|
|
20
20
|
# - inner_type: the Rigor::Type returned to the caller (currently
|
|
21
21
|
# always Dynamic[Top]; later slices may carry richer fallback
|
|
22
22
|
# types).
|
|
23
|
-
Fallback
|
|
23
|
+
class Fallback < Data.define(:node_class, :location, :family, :inner_type)
|
|
24
24
|
def initialize(node_class:, location:, family:, inner_type:)
|
|
25
25
|
raise ArgumentError, "node_class must be a Class, got #{node_class.class}" unless node_class.is_a?(Class)
|
|
26
26
|
|