rigortype 0.1.3 → 0.1.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 +154 -33
- 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 +26 -6
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +498 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +364 -55
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +39 -6
- 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 +61 -3
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +109 -6
- 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/acceptance.rb +35 -1
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +77 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
- data/lib/rigor/inference/method_dispatcher.rb +274 -5
- data/lib/rigor/inference/method_parameter_binder.rb +22 -14
- data/lib/rigor/inference/narrowing.rb +129 -12
- 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/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +5 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +102 -10
- data/lib/rigor/plugin/registry.rb +43 -2
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +2 -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/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +207 -3
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +68 -0
- data/lib/rigor/type_node/identifier.rb +38 -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 +32 -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 +8 -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/blueprint.rbs +7 -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 +16 -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
- data/sig/rigor.rbs +35 -2
- metadata +90 -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)
|
|
@@ -81,15 +82,36 @@ module Rigor
|
|
|
81
82
|
runner = Analysis::Runner.new(
|
|
82
83
|
configuration: configuration,
|
|
83
84
|
explain: options.fetch(:explain),
|
|
84
|
-
cache_store: cache_store
|
|
85
|
+
cache_store: cache_store,
|
|
86
|
+
collect_stats: options.fetch(:stats),
|
|
87
|
+
workers: resolve_workers(options, configuration)
|
|
85
88
|
)
|
|
86
89
|
result = runner.run(paths)
|
|
87
90
|
|
|
88
91
|
write_result(result, options.fetch(:format))
|
|
92
|
+
write_run_stats(result.stats) if result.stats
|
|
89
93
|
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
90
94
|
result.success? ? 0 : 1
|
|
91
95
|
end
|
|
92
96
|
|
|
97
|
+
# ADR-15 Phase 4c — resolves the worker count by
|
|
98
|
+
# precedence: CLI `--workers=N` (most explicit) > env
|
|
99
|
+
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
100
|
+
# `parallel.workers:` > 0 (sequential default). Returns
|
|
101
|
+
# an Integer; non-numeric values raise so typos fail
|
|
102
|
+
# loudly. CLI / env may pass a negative value — clamped
|
|
103
|
+
# to 0 (sequential) so a stray `-1` doesn't crash the
|
|
104
|
+
# pool spawn loop.
|
|
105
|
+
def resolve_workers(options, configuration)
|
|
106
|
+
cli_value = options[:workers]
|
|
107
|
+
return [Integer(cli_value), 0].max if cli_value
|
|
108
|
+
|
|
109
|
+
env_value = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
110
|
+
return [Integer(env_value), 0].max if env_value && !env_value.empty?
|
|
111
|
+
|
|
112
|
+
configuration.parallel_workers
|
|
113
|
+
end
|
|
114
|
+
|
|
93
115
|
def parse_check_options
|
|
94
116
|
options = {
|
|
95
117
|
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
@@ -99,7 +121,19 @@ module Rigor
|
|
|
99
121
|
explain: false,
|
|
100
122
|
cache_stats: false,
|
|
101
123
|
clear_cache: false,
|
|
102
|
-
no_cache: false
|
|
124
|
+
no_cache: false,
|
|
125
|
+
# Run-stats summary (target files, RBS class universe
|
|
126
|
+
# breakdown, wall time, peak RSS) is on by default
|
|
127
|
+
# because collection is ~free (single syscall for RSS,
|
|
128
|
+
# one walk of `class_decl_paths` for the breakdown).
|
|
129
|
+
# `--no-stats` suppresses it for callers that want a
|
|
130
|
+
# diagnostic-only output stream.
|
|
131
|
+
stats: true,
|
|
132
|
+
# ADR-15 Phase 4c — when nil, falls back to
|
|
133
|
+
# `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
|
|
134
|
+
# `parallel.workers:` then 0 (sequential). See
|
|
135
|
+
# `resolve_workers` for the precedence chain.
|
|
136
|
+
workers: nil
|
|
103
137
|
}
|
|
104
138
|
parser = OptionParser.new do |opts|
|
|
105
139
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
@@ -109,6 +143,14 @@ module Rigor
|
|
|
109
143
|
opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
|
|
110
144
|
opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
|
|
111
145
|
opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
|
|
146
|
+
opts.on("--[no-]stats",
|
|
147
|
+
"Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
|
|
148
|
+
options[:stats] = value
|
|
149
|
+
end
|
|
150
|
+
opts.on("--workers=N", Integer,
|
|
151
|
+
"Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
|
|
152
|
+
options[:workers] = value
|
|
153
|
+
end
|
|
112
154
|
end
|
|
113
155
|
parser.parse!(@argv)
|
|
114
156
|
options
|
|
@@ -123,6 +165,15 @@ module Rigor
|
|
|
123
165
|
end
|
|
124
166
|
end
|
|
125
167
|
|
|
168
|
+
# Emits the {Analysis::RunStats} summary to STDERR so it
|
|
169
|
+
# doesn't interleave with the diagnostic stream (text or
|
|
170
|
+
# JSON) on STDOUT. JSON consumers can pipe stdout cleanly;
|
|
171
|
+
# interactive users still see the summary on their tty.
|
|
172
|
+
def write_run_stats(stats)
|
|
173
|
+
@err.puts("")
|
|
174
|
+
stats.format(@err)
|
|
175
|
+
end
|
|
176
|
+
|
|
126
177
|
def write_cache_stats(cache_root, runtime_store)
|
|
127
178
|
inv = Cache::Store.disk_inventory(root: cache_root)
|
|
128
179
|
|
|
@@ -279,6 +330,12 @@ module Rigor
|
|
|
279
330
|
DiffCommand.new(argv: @argv, out: @out, err: @err).run
|
|
280
331
|
end
|
|
281
332
|
|
|
333
|
+
def run_sig_gen
|
|
334
|
+
require_relative "cli/sig_gen_command"
|
|
335
|
+
|
|
336
|
+
SigGenCommand.new(argv: @argv, out: @out, err: @err).run
|
|
337
|
+
end
|
|
338
|
+
|
|
282
339
|
def write_result(result, format)
|
|
283
340
|
case format
|
|
284
341
|
when "json"
|
|
@@ -318,6 +375,7 @@ module Rigor
|
|
|
318
375
|
type-scan Report Scope#type_of coverage across PATHs
|
|
319
376
|
explain Print the description of one or all CheckRules
|
|
320
377
|
diff Compare current diagnostics to a saved baseline JSON
|
|
378
|
+
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
321
379
|
version Print the Rigor version
|
|
322
380
|
help Print this help
|
|
323
381
|
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
|
@@ -63,6 +63,81 @@ module Rigor
|
|
|
63
63
|
"dependencies" => {
|
|
64
64
|
"source_inference" => [],
|
|
65
65
|
"budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
|
|
66
|
+
},
|
|
67
|
+
"parallel" => {
|
|
68
|
+
# ADR-15 Phase 4c — when greater than zero, `rigor check`
|
|
69
|
+
# dispatches per-file analysis across N Ractor workers
|
|
70
|
+
# built around {Rigor::Analysis::WorkerSession}.
|
|
71
|
+
# `0` (default) keeps the sequential coordinator path
|
|
72
|
+
# bit-for-bit unchanged. The CLI's `--workers=N` flag
|
|
73
|
+
# and the `RIGOR_RACTOR_WORKERS` env var both override
|
|
74
|
+
# this setting; precedence is CLI > env > config > 0.
|
|
75
|
+
"workers" => 0
|
|
76
|
+
},
|
|
77
|
+
"bundler" => {
|
|
78
|
+
# Open item O4 — target-project Bundler awareness.
|
|
79
|
+
# When `bundle_path:` is set (or auto-detected), Rigor
|
|
80
|
+
# walks `<bundle_path>/ruby/*/gems/*/sig/` and adds each
|
|
81
|
+
# gem-shipped sig directory to `signature_paths:`. With
|
|
82
|
+
# O7's failure-memo in place, conflicts (a vendored sig
|
|
83
|
+
# already declares the same constant) degrade gracefully
|
|
84
|
+
# to "no RBS env" with a single-line warning naming the
|
|
85
|
+
# offending file, rather than hanging.
|
|
86
|
+
#
|
|
87
|
+
# `bundle_path:` (String, optional): explicit path to the
|
|
88
|
+
# bundler install root (e.g., "vendor/bundle" or an
|
|
89
|
+
# absolute path). Resolved relative to the project root
|
|
90
|
+
# (`paths:`'s base) when relative.
|
|
91
|
+
#
|
|
92
|
+
# `auto_detect:` (Boolean, default true): when no
|
|
93
|
+
# explicit `bundle_path:` is set, try `.bundle/config`'s
|
|
94
|
+
# `BUNDLE_PATH:` first; fall back to `vendor/bundle/`
|
|
95
|
+
# under the project root if it exists. When neither is
|
|
96
|
+
# found, no extra sigs are added — the analyzer sees
|
|
97
|
+
# only rigor's vendored RBS and the user's
|
|
98
|
+
# `signature_paths:`.
|
|
99
|
+
#
|
|
100
|
+
# O4 Layer 3 keys:
|
|
101
|
+
#
|
|
102
|
+
# `lockfile:` (String, optional): explicit path to a
|
|
103
|
+
# `Gemfile.lock`. Resolved relative to the project root
|
|
104
|
+
# when relative. When set (or auto-detected via the
|
|
105
|
+
# `auto_detect:` flag below) Rigor parses the lockfile
|
|
106
|
+
# and uses it to FILTER the bundle-discovered `sig/`
|
|
107
|
+
# directories: only gems whose `(name, version,
|
|
108
|
+
# platform)` matches a lockfile entry are admitted to
|
|
109
|
+
# `signature_paths:`. Stale or out-of-band gems sitting
|
|
110
|
+
# in the bundle install tree are silently dropped.
|
|
111
|
+
#
|
|
112
|
+
# `auto_detect:` (Boolean, also gates the lockfile
|
|
113
|
+
# search): when true and `lockfile:` is nil, look for
|
|
114
|
+
# `<project_root>/Gemfile.lock`.
|
|
115
|
+
"bundle_path" => nil,
|
|
116
|
+
"auto_detect" => true,
|
|
117
|
+
"lockfile" => nil
|
|
118
|
+
},
|
|
119
|
+
"rbs_collection" => {
|
|
120
|
+
# Open item O4 Layer 3 slice 2 — `rbs collection
|
|
121
|
+
# install` awareness. When the target project has been
|
|
122
|
+
# set up with `rbs collection install`, the resulting
|
|
123
|
+
# `rbs_collection.lock.yaml` carries the resolved (gem,
|
|
124
|
+
# version, source) triples and `.gem_rbs_collection/`
|
|
125
|
+
# holds the downloaded `.rbs` files. Rigor parses the
|
|
126
|
+
# lockfile and auto-feeds each gem's
|
|
127
|
+
# `<collection_root>/<name>/<version>/` directory into
|
|
128
|
+
# `RbsLoader`'s `signature_paths:`. Sources of type
|
|
129
|
+
# `stdlib` are skipped because rigor's bundled
|
|
130
|
+
# `DEFAULT_LIBRARIES` already covers that surface.
|
|
131
|
+
#
|
|
132
|
+
# `lockfile:` (String, optional): explicit path to
|
|
133
|
+
# `rbs_collection.lock.yaml`. Resolved relative to the
|
|
134
|
+
# project root when relative.
|
|
135
|
+
#
|
|
136
|
+
# `auto_detect:` (Boolean, default true): when no
|
|
137
|
+
# explicit `lockfile:` is set, look for
|
|
138
|
+
# `<project_root>/rbs_collection.lock.yaml`.
|
|
139
|
+
"lockfile" => nil,
|
|
140
|
+
"auto_detect" => true
|
|
66
141
|
}
|
|
67
142
|
}.freeze
|
|
68
143
|
|
|
@@ -78,7 +153,9 @@ module Rigor
|
|
|
78
153
|
:plugins_io_network, :plugins_io_allowed_paths,
|
|
79
154
|
:plugins_io_allowed_url_hosts,
|
|
80
155
|
:severity_profile, :severity_overrides,
|
|
81
|
-
:dependencies
|
|
156
|
+
:dependencies, :parallel_workers,
|
|
157
|
+
:bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
|
|
158
|
+
:rbs_collection_lockfile, :rbs_collection_auto_detect
|
|
82
159
|
|
|
83
160
|
# Loads a configuration file.
|
|
84
161
|
#
|
|
@@ -214,13 +291,13 @@ module Rigor
|
|
|
214
291
|
private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
|
|
215
292
|
:merge_value, :merge_dependencies_hash
|
|
216
293
|
|
|
217
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/
|
|
294
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
218
295
|
def initialize(data = DEFAULTS)
|
|
219
296
|
cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
|
|
220
297
|
plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
|
|
221
298
|
|
|
222
299
|
@target_ruby = coerce_target_ruby(data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")))
|
|
223
|
-
@paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s)
|
|
300
|
+
@paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s).freeze
|
|
224
301
|
user_excludes = Array(data.fetch("exclude", DEFAULTS.fetch("exclude"))).map(&:to_s)
|
|
225
302
|
@exclude_patterns = (BUILTIN_EXCLUDES + user_excludes).uniq.freeze
|
|
226
303
|
@plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map do |entry|
|
|
@@ -246,10 +323,32 @@ module Rigor
|
|
|
246
323
|
@dependencies = Dependencies.from_h(
|
|
247
324
|
data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
|
|
248
325
|
)
|
|
326
|
+
parallel = DEFAULTS.fetch("parallel").merge(data.fetch("parallel", {}))
|
|
327
|
+
@parallel_workers = coerce_parallel_workers(parallel.fetch("workers"))
|
|
328
|
+
bundler = DEFAULTS.fetch("bundler").merge(data.fetch("bundler", {}))
|
|
329
|
+
bp = bundler.fetch("bundle_path")
|
|
330
|
+
@bundler_bundle_path = bp.nil? ? nil : bp.to_s.dup.freeze
|
|
331
|
+
@bundler_auto_detect = bundler.fetch("auto_detect") == true
|
|
332
|
+
lf = bundler.fetch("lockfile")
|
|
333
|
+
@bundler_lockfile = lf.nil? ? nil : lf.to_s.dup.freeze
|
|
334
|
+
rbs_collection = DEFAULTS.fetch("rbs_collection").merge(data.fetch("rbs_collection", {}))
|
|
335
|
+
rclf = rbs_collection.fetch("lockfile")
|
|
336
|
+
@rbs_collection_lockfile = rclf.nil? ? nil : rclf.to_s.dup.freeze
|
|
337
|
+
@rbs_collection_auto_detect = rbs_collection.fetch("auto_detect") == true
|
|
338
|
+
# Ractor migration Phase 2a: deep-freeze the
|
|
339
|
+
# Configuration so it is `Ractor.shareable?`. Every
|
|
340
|
+
# ivar above is now either a frozen value (Symbol /
|
|
341
|
+
# nil / Boolean) or an explicitly frozen
|
|
342
|
+
# collection / value object; freezing `self` makes the
|
|
343
|
+
# whole carrier safe to send across Ractor boundaries
|
|
344
|
+
# (and catches accidental post-init mutation in any
|
|
345
|
+
# caller). See
|
|
346
|
+
# `docs/design/20260514-ractor-migration.md`.
|
|
347
|
+
freeze
|
|
249
348
|
end
|
|
250
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/
|
|
349
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
251
350
|
|
|
252
|
-
def to_h
|
|
351
|
+
def to_h # rubocop:disable Metrics/MethodLength
|
|
253
352
|
{
|
|
254
353
|
"target_ruby" => target_ruby,
|
|
255
354
|
"paths" => paths,
|
|
@@ -269,7 +368,19 @@ module Rigor
|
|
|
269
368
|
},
|
|
270
369
|
"severity_profile" => severity_profile.to_s,
|
|
271
370
|
"severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
|
|
272
|
-
"dependencies" => dependencies.to_h
|
|
371
|
+
"dependencies" => dependencies.to_h,
|
|
372
|
+
"parallel" => {
|
|
373
|
+
"workers" => parallel_workers
|
|
374
|
+
},
|
|
375
|
+
"bundler" => {
|
|
376
|
+
"bundle_path" => bundler_bundle_path,
|
|
377
|
+
"auto_detect" => bundler_auto_detect,
|
|
378
|
+
"lockfile" => bundler_lockfile
|
|
379
|
+
},
|
|
380
|
+
"rbs_collection" => {
|
|
381
|
+
"lockfile" => rbs_collection_lockfile,
|
|
382
|
+
"auto_detect" => rbs_collection_auto_detect
|
|
383
|
+
}
|
|
273
384
|
}
|
|
274
385
|
end
|
|
275
386
|
|
|
@@ -327,6 +438,20 @@ module Rigor
|
|
|
327
438
|
VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
|
|
328
439
|
private_constant :VALID_NETWORK_POLICIES
|
|
329
440
|
|
|
441
|
+
# ADR-15 Phase 4c — accepts a non-negative Integer (or a
|
|
442
|
+
# string-shaped one from YAML files that miss type
|
|
443
|
+
# annotations). Negative / non-integer values raise so
|
|
444
|
+
# typos / bad YAML fail loudly rather than silently
|
|
445
|
+
# disabling parallelism.
|
|
446
|
+
def coerce_parallel_workers(value)
|
|
447
|
+
integer = Integer(value)
|
|
448
|
+
raise ArgumentError, "parallel.workers must be >= 0, got #{value.inspect}" if integer.negative?
|
|
449
|
+
|
|
450
|
+
integer
|
|
451
|
+
rescue TypeError, ArgumentError => e
|
|
452
|
+
raise ArgumentError, "parallel.workers must be a non-negative Integer, got #{value.inspect} (#{e.message})"
|
|
453
|
+
end
|
|
454
|
+
|
|
330
455
|
def coerce_network_policy(value)
|
|
331
456
|
sym = value.to_sym
|
|
332
457
|
unless VALID_NETWORK_POLICIES.include?(sym)
|