rigortype 0.1.9 → 0.1.11
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/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +57 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli.rb +88 -5
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +159 -4
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +143 -12
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +115 -7
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +26 -2
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +28 -5
- data/lib/rigor/plugin/manifest.rb +33 -5
- data/lib/rigor/plugin/registry.rb +21 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +178 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +310 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +76 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +177 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +589 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +150 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +123 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +247 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +273 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +240 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +94 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +514 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +34 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +463 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +277 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +167 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +161 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +103 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +490 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +158 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +180 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
require "prism"
|
|
5
|
+
|
|
6
|
+
require_relative "../configuration"
|
|
7
|
+
require_relative "../environment"
|
|
8
|
+
require_relative "../inference/precision_scanner"
|
|
9
|
+
require_relative "../scope"
|
|
10
|
+
require_relative "coverage_report"
|
|
11
|
+
require_relative "coverage_renderer"
|
|
12
|
+
|
|
13
|
+
module Rigor
|
|
14
|
+
class CLI
|
|
15
|
+
# Executes the `rigor coverage` command.
|
|
16
|
+
#
|
|
17
|
+
# Walks every Prism node in one or more files, infers its type via
|
|
18
|
+
# `Rigor::Scope#type_of`, and classifies the result into precision tiers
|
|
19
|
+
# (constant / nominal / shaped / refined / bot / dynamic_specific /
|
|
20
|
+
# dynamic_top / top). Reports aggregate and per-file statistics so
|
|
21
|
+
# maintainers can track type-precision trends and SKILL pipelines can
|
|
22
|
+
# measure the impact of adding new constant-fold or shape-dispatch rules.
|
|
23
|
+
#
|
|
24
|
+
# Exit codes:
|
|
25
|
+
# 0 — scan complete, precision ratio ≥ threshold (or no threshold given)
|
|
26
|
+
# 1 — precision ratio < threshold, or parse errors encountered
|
|
27
|
+
# 64 — usage error
|
|
28
|
+
class CoverageCommand
|
|
29
|
+
USAGE = "Usage: rigor coverage [options] PATH..."
|
|
30
|
+
|
|
31
|
+
def initialize(argv:, out:, err:)
|
|
32
|
+
@argv = argv
|
|
33
|
+
@out = out
|
|
34
|
+
@err = err
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Integer] CLI exit status.
|
|
38
|
+
def run
|
|
39
|
+
options = parse_options
|
|
40
|
+
paths = collect_paths(@argv)
|
|
41
|
+
return CLI::EXIT_USAGE if paths.nil?
|
|
42
|
+
return usage_error if paths.empty?
|
|
43
|
+
|
|
44
|
+
report = scan_paths(paths, options)
|
|
45
|
+
CoverageRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
46
|
+
determine_exit(report, options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def parse_options
|
|
52
|
+
options = { format: "text", threshold: nil, config: nil }
|
|
53
|
+
|
|
54
|
+
OptionParser.new do |opts|
|
|
55
|
+
opts.banner = USAGE
|
|
56
|
+
opts.on("--format=FORMAT", "Output format: text or json") { |v| options[:format] = v }
|
|
57
|
+
opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
|
|
58
|
+
opts.on(
|
|
59
|
+
"--threshold=RATIO", Float,
|
|
60
|
+
"Exit 1 when precision ratio is below RATIO (0.0–1.0)"
|
|
61
|
+
) { |v| options[:threshold] = v }
|
|
62
|
+
end.parse!(@argv)
|
|
63
|
+
|
|
64
|
+
options
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collect_paths(args)
|
|
68
|
+
paths = []
|
|
69
|
+
args.each do |arg|
|
|
70
|
+
if File.directory?(arg)
|
|
71
|
+
paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
|
|
72
|
+
elsif File.file?(arg)
|
|
73
|
+
paths << arg
|
|
74
|
+
else
|
|
75
|
+
@err.puts("coverage: not a file or directory: #{arg}")
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
paths.uniq
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def usage_error
|
|
83
|
+
@err.puts("coverage: at least one path is required")
|
|
84
|
+
@err.puts(USAGE)
|
|
85
|
+
CLI::EXIT_USAGE
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def scan_paths(paths, options)
|
|
89
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
90
|
+
scope = Scope.empty(environment: project_environment(configuration))
|
|
91
|
+
scanner = Inference::PrecisionScanner.new(scope: scope)
|
|
92
|
+
accumulator = CoverageAccumulator.new
|
|
93
|
+
|
|
94
|
+
paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
|
|
95
|
+
accumulator.to_report(paths, options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def project_environment(configuration)
|
|
99
|
+
Environment.for_project(
|
|
100
|
+
libraries: configuration.libraries,
|
|
101
|
+
signature_paths: configuration.signature_paths
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def scan_one(path, scanner, accumulator, configuration)
|
|
106
|
+
source = File.read(path)
|
|
107
|
+
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
108
|
+
if parse_result.errors.any?
|
|
109
|
+
accumulator.record_parse_error(path, parse_result.errors)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
accumulator.absorb(path, scanner.scan(parse_result.value))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def determine_exit(report, options)
|
|
117
|
+
return 1 unless report.parse_errors.empty?
|
|
118
|
+
|
|
119
|
+
threshold = options[:threshold]
|
|
120
|
+
return 0 if threshold.nil?
|
|
121
|
+
|
|
122
|
+
report.precision_ratio < threshold ? 1 : 0
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renders a `CoverageReport` as terminal-friendly text or JSON.
|
|
8
|
+
class CoverageRenderer
|
|
9
|
+
TIER_LABELS = {
|
|
10
|
+
constant: "constant",
|
|
11
|
+
nominal: "nominal",
|
|
12
|
+
shaped: "shaped (Tuple/Hash/Range/generic)",
|
|
13
|
+
refined: "refined",
|
|
14
|
+
bot: "bot (unreachable)",
|
|
15
|
+
dynamic_specific: "dynamic — partial info",
|
|
16
|
+
dynamic_top: "dynamic — opaque (untyped)",
|
|
17
|
+
top: "top"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(out:)
|
|
21
|
+
@out = out
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render(report, format:)
|
|
25
|
+
case format
|
|
26
|
+
when "text" then render_text(report)
|
|
27
|
+
when "json" then render_json(report)
|
|
28
|
+
else raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def render_text(report)
|
|
35
|
+
render_text_header(report)
|
|
36
|
+
render_text_summary(report)
|
|
37
|
+
render_text_tier_table(report)
|
|
38
|
+
render_text_per_file(report) if report.per_file.size > 1
|
|
39
|
+
render_text_parse_errors(report)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_text_header(report)
|
|
43
|
+
n = report.files.size
|
|
44
|
+
suffix = n == 1 ? "" : "s"
|
|
45
|
+
@out.puts("Type coverage: #{n} file#{suffix}")
|
|
46
|
+
report.files.first(5).each { |f| @out.puts(" - #{f}") }
|
|
47
|
+
@out.puts(" ... (#{n - 5} more)") if n > 5
|
|
48
|
+
@out.puts
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render_text_summary(report)
|
|
52
|
+
g = report.grand_total
|
|
53
|
+
p = report.precise_count
|
|
54
|
+
o = report.opaque_count
|
|
55
|
+
@out.puts("Summary:")
|
|
56
|
+
@out.puts(" files processed: #{report.files.size - report.parse_errors.size}")
|
|
57
|
+
@out.puts(" parse errors: #{report.parse_errors.size}")
|
|
58
|
+
@out.puts(" expressions typed: #{g}")
|
|
59
|
+
@out.puts(" precise: #{p}#{pct(p, g)}")
|
|
60
|
+
@out.puts(" dynamic (opaque): #{o}#{pct(o, g)}")
|
|
61
|
+
@out.puts(" precision ratio: #{(report.precision_ratio * 100).round(2)}%")
|
|
62
|
+
@out.puts
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_text_tier_table(report)
|
|
66
|
+
@out.puts("Tier breakdown:")
|
|
67
|
+
g = report.grand_total
|
|
68
|
+
Inference::PrecisionScanner::TIERS.each do |tier|
|
|
69
|
+
n = report.tier_count(tier)
|
|
70
|
+
next if n.zero?
|
|
71
|
+
|
|
72
|
+
label = TIER_LABELS.fetch(tier, tier.to_s).ljust(36)
|
|
73
|
+
@out.puts(" #{label} #{n.to_s.rjust(7)}#{pct(n, g)}")
|
|
74
|
+
end
|
|
75
|
+
@out.puts
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_text_per_file(report)
|
|
79
|
+
@out.puts("Per-file breakdown:")
|
|
80
|
+
width = report.per_file.map { |e| e[:file].size }.max || 0
|
|
81
|
+
report.per_file.sort_by { |e| e[:result].precision_ratio }.each do |entry|
|
|
82
|
+
r = entry[:result]
|
|
83
|
+
next if r.total.zero?
|
|
84
|
+
|
|
85
|
+
ratio_str = "#{(r.precision_ratio * 100).round(1)}%".rjust(6)
|
|
86
|
+
@out.puts(" #{entry[:file].ljust(width)} #{ratio_str} (#{r.precise_count}/#{r.total})")
|
|
87
|
+
end
|
|
88
|
+
@out.puts
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_text_parse_errors(report)
|
|
92
|
+
return if report.parse_errors.empty?
|
|
93
|
+
|
|
94
|
+
@out.puts("Parse errors:")
|
|
95
|
+
report.parse_errors.each do |entry|
|
|
96
|
+
@out.puts(" #{entry[:file]}: #{entry[:errors].join('; ')}")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_json(report)
|
|
101
|
+
@out.puts(JSON.pretty_generate(json_payload(report)))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def json_payload(report)
|
|
105
|
+
g = report.grand_total
|
|
106
|
+
{
|
|
107
|
+
summary: json_summary(report, g),
|
|
108
|
+
by_tier: tier_payload(g) { |tier| report.tier_count(tier) },
|
|
109
|
+
by_file: report.per_file.map { |e| file_payload(e) },
|
|
110
|
+
parse_errors: report.parse_errors.map { |e| { file: e[:file], errors: e[:errors] } }
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def json_summary(report, grand_total)
|
|
115
|
+
g = grand_total
|
|
116
|
+
dsc = report.total.dynamic_specific_count
|
|
117
|
+
{
|
|
118
|
+
files_processed: report.files.size - report.parse_errors.size,
|
|
119
|
+
parse_errors: report.parse_errors.size,
|
|
120
|
+
expressions_typed: g,
|
|
121
|
+
precise_count: report.precise_count,
|
|
122
|
+
precise_ratio: ratio_f(report.precision_ratio),
|
|
123
|
+
dynamic_opaque_count: report.opaque_count,
|
|
124
|
+
dynamic_opaque_ratio: ratio_f(report.opaque_ratio),
|
|
125
|
+
dynamic_specific_count: dsc,
|
|
126
|
+
dynamic_specific_ratio: ratio_f(dsc.fdiv(g.nonzero? || 1))
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def tier_payload(grand_total)
|
|
131
|
+
g = grand_total
|
|
132
|
+
Inference::PrecisionScanner::TIERS.to_h do |tier|
|
|
133
|
+
n = yield tier
|
|
134
|
+
[tier, { count: n, ratio: ratio_f(n.fdiv(g.nonzero? || 1)) }]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def file_payload(entry)
|
|
139
|
+
r = entry[:result]
|
|
140
|
+
{
|
|
141
|
+
file: entry[:file],
|
|
142
|
+
expressions_typed: r.total,
|
|
143
|
+
precise_count: r.precise_count,
|
|
144
|
+
precise_ratio: ratio_f(r.precision_ratio),
|
|
145
|
+
dynamic_opaque_count: r.opaque_count,
|
|
146
|
+
dynamic_opaque_ratio: ratio_f(r.opaque_ratio),
|
|
147
|
+
by_tier: tier_payload(r.total) { |tier| r.tier_counts.fetch(tier, 0) }
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def pct(numerator, denominator)
|
|
152
|
+
return "" if denominator.zero?
|
|
153
|
+
|
|
154
|
+
" (#{(numerator.fdiv(denominator) * 100).round(1)}%)"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def ratio_f(val)
|
|
158
|
+
val.round(4)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# Aggregated precision-coverage report assembled by `CoverageCommand`.
|
|
6
|
+
# Holds per-file breakdowns and accumulated totals; consumed by
|
|
7
|
+
# `CoverageRenderer` for text and JSON output.
|
|
8
|
+
class CoverageReport < Data.define(
|
|
9
|
+
:files,
|
|
10
|
+
:parse_errors,
|
|
11
|
+
:per_file,
|
|
12
|
+
:total
|
|
13
|
+
)
|
|
14
|
+
# Sum of all per-file totals.
|
|
15
|
+
def grand_total
|
|
16
|
+
total.total
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def precise_count
|
|
20
|
+
total.precise_count
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def opaque_count
|
|
24
|
+
total.opaque_count
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def precision_ratio
|
|
28
|
+
total.precision_ratio
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def opaque_ratio
|
|
32
|
+
total.opaque_ratio
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tier_count(tier)
|
|
36
|
+
total.tier_counts.fetch(tier, 0)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Mutable accumulator used while scanning files.
|
|
41
|
+
class CoverageAccumulator
|
|
42
|
+
require_relative "../inference/precision_scanner"
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@per_file = []
|
|
46
|
+
@parse_errors = []
|
|
47
|
+
# Accumulated totals across all files.
|
|
48
|
+
@total_total = 0
|
|
49
|
+
@total_tier_counts = Inference::PrecisionScanner::TIERS.to_h { |t| [t, 0] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def absorb(path, file_result)
|
|
53
|
+
@per_file << { file: path, result: file_result }
|
|
54
|
+
@total_total += file_result.total
|
|
55
|
+
file_result.tier_counts.each { |tier, n| @total_tier_counts[tier] += n }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def record_parse_error(path, errors)
|
|
59
|
+
@parse_errors << { file: path, errors: errors.map(&:message) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_report(files, _options)
|
|
63
|
+
CoverageReport.new(
|
|
64
|
+
files: files,
|
|
65
|
+
parse_errors: @parse_errors,
|
|
66
|
+
per_file: @per_file,
|
|
67
|
+
total: Inference::PrecisionScanner::FileResult.new(
|
|
68
|
+
total: @total_total,
|
|
69
|
+
tier_counts: @total_tier_counts
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Executes the `rigor mcp` command.
|
|
8
|
+
#
|
|
9
|
+
# Starts a long-running MCP (Model Context Protocol) server over stdio.
|
|
10
|
+
# The server exposes Rigor's analysis tools as MCP tool calls over a
|
|
11
|
+
# newline-delimited JSON-RPC 2.0 stream. See ADR-33.
|
|
12
|
+
#
|
|
13
|
+
# Slice 1 ships the stdio transport with seven read-only tools:
|
|
14
|
+
# rigor_check, rigor_type_of, rigor_triage, rigor_annotate,
|
|
15
|
+
# rigor_sig_gen, rigor_explain, rigor_coverage.
|
|
16
|
+
class McpCommand
|
|
17
|
+
USAGE = "Usage: rigor mcp [options]"
|
|
18
|
+
|
|
19
|
+
def initialize(argv:, out:, err:)
|
|
20
|
+
@argv = argv
|
|
21
|
+
@out = out
|
|
22
|
+
@err = err
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Integer] CLI exit status.
|
|
26
|
+
def run
|
|
27
|
+
options = parse_options
|
|
28
|
+
return CLI::EXIT_USAGE if options == :usage_error
|
|
29
|
+
|
|
30
|
+
transport = options.fetch(:transport)
|
|
31
|
+
unless transport == "stdio"
|
|
32
|
+
@err.puts("rigor mcp: unsupported transport: #{transport.inspect} (only `stdio` is supported in v1)")
|
|
33
|
+
return CLI::EXIT_USAGE
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require_relative "../mcp"
|
|
37
|
+
require_relative "../version"
|
|
38
|
+
|
|
39
|
+
server = MCP::Server.new(config_path: options.fetch(:config), err: $stderr)
|
|
40
|
+
loop_runner = MCP::Loop.new(input: $stdin, output: $stdout, server: server)
|
|
41
|
+
loop_runner.run
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parse_options
|
|
48
|
+
options = { transport: "stdio", config: nil }
|
|
49
|
+
|
|
50
|
+
parser = OptionParser.new do |opts|
|
|
51
|
+
opts.banner = USAGE
|
|
52
|
+
opts.on("--transport=NAME",
|
|
53
|
+
"Transport (default: stdio; only stdio is supported in v1)") do |value|
|
|
54
|
+
options[:transport] = value
|
|
55
|
+
end
|
|
56
|
+
opts.on("--config=PATH",
|
|
57
|
+
"Session-level default config path (individual tool calls may override)") do |value|
|
|
58
|
+
options[:config] = value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
parser.parse!(@argv)
|
|
62
|
+
options
|
|
63
|
+
rescue OptionParser::ParseError => e
|
|
64
|
+
@err.puts(e.message)
|
|
65
|
+
@err.puts(USAGE)
|
|
66
|
+
:usage_error
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -28,8 +28,11 @@ module Rigor
|
|
|
28
28
|
"diff" => :run_diff,
|
|
29
29
|
"sig-gen" => :run_sig_gen,
|
|
30
30
|
"lsp" => :run_lsp,
|
|
31
|
+
"mcp" => :run_mcp,
|
|
31
32
|
"baseline" => :run_baseline,
|
|
32
|
-
"triage" => :run_triage
|
|
33
|
+
"triage" => :run_triage,
|
|
34
|
+
"coverage" => :run_coverage,
|
|
35
|
+
"playground" => :run_playground
|
|
33
36
|
}.freeze
|
|
34
37
|
|
|
35
38
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -81,7 +84,7 @@ module Rigor
|
|
|
81
84
|
buffer = resolve_buffer_binding(options)
|
|
82
85
|
return EXIT_USAGE if buffer == :usage_error
|
|
83
86
|
|
|
84
|
-
configuration =
|
|
87
|
+
configuration = load_check_configuration(options)
|
|
85
88
|
cache_root = configuration.cache_path
|
|
86
89
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
87
90
|
|
|
@@ -118,7 +121,7 @@ module Rigor
|
|
|
118
121
|
return false
|
|
119
122
|
end
|
|
120
123
|
|
|
121
|
-
baseline = Analysis::Baseline.load(path)
|
|
124
|
+
baseline = Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
122
125
|
return false if baseline.nil? || baseline.empty?
|
|
123
126
|
|
|
124
127
|
drifted = baseline.audit(raw_diagnostics).reject { |row| row.status == :within }
|
|
@@ -161,7 +164,7 @@ module Rigor
|
|
|
161
164
|
path = resolve_baseline_path(configuration, options)
|
|
162
165
|
return result if path.nil?
|
|
163
166
|
|
|
164
|
-
baseline = Analysis::Baseline.load(path)
|
|
167
|
+
baseline = Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
165
168
|
return result if baseline.nil?
|
|
166
169
|
|
|
167
170
|
surfaced, silenced_count = baseline.filter(result.diagnostics)
|
|
@@ -281,7 +284,16 @@ module Rigor
|
|
|
281
284
|
baseline: :unset,
|
|
282
285
|
# ADR-22 slice 5 — `--baseline-strict` CI gate: fail the
|
|
283
286
|
# run on any baseline drift, in either direction.
|
|
284
|
-
baseline_strict: false
|
|
287
|
+
baseline_strict: false,
|
|
288
|
+
# ADR-32 WD10 carry-over — `--treat-all-as-inline-rbs`
|
|
289
|
+
# forces the `rigor-rbs-inline` plugin into the loaded
|
|
290
|
+
# plugin set with `require_magic_comment: false` so a
|
|
291
|
+
# single ad-hoc `rigor check` invocation treats every
|
|
292
|
+
# analysed file as inline-RBS without the user editing
|
|
293
|
+
# `.rigor.yml`. Intended for single-file / ad-hoc CI use;
|
|
294
|
+
# ordinary projects should configure the plugin in
|
|
295
|
+
# `.rigor.yml`.
|
|
296
|
+
treat_all_as_inline_rbs: false
|
|
285
297
|
}
|
|
286
298
|
parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
287
299
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
@@ -319,11 +331,56 @@ module Rigor
|
|
|
319
331
|
"ADR-22: fail the run on any baseline drift (CI gate)") do
|
|
320
332
|
options[:baseline_strict] = true
|
|
321
333
|
end
|
|
334
|
+
opts.on("--treat-all-as-inline-rbs",
|
|
335
|
+
"ADR-32: force-load rigor-rbs-inline with require_magic_comment: false") do
|
|
336
|
+
options[:treat_all_as_inline_rbs] = true
|
|
337
|
+
end
|
|
322
338
|
end
|
|
323
339
|
parser.parse!(@argv)
|
|
324
340
|
options
|
|
325
341
|
end
|
|
326
342
|
|
|
343
|
+
# ADR-32 WD10 carry-over — wraps `Configuration.load` so the
|
|
344
|
+
# CLI's `--treat-all-as-inline-rbs` flag can inject a
|
|
345
|
+
# `rigor-rbs-inline` plugin entry with
|
|
346
|
+
# `require_magic_comment: false` into the loaded plugin
|
|
347
|
+
# set. Re-runs the include-aware YAML load and applies the
|
|
348
|
+
# injection before `Configuration.new` so the new entry
|
|
349
|
+
# follows the normal coercion path. A pre-existing
|
|
350
|
+
# `rigor-rbs-inline` entry (by gem name or `id: rbs-inline`)
|
|
351
|
+
# is removed first so the synthesised entry's
|
|
352
|
+
# `require_magic_comment: false` wins unconditionally.
|
|
353
|
+
def load_check_configuration(options)
|
|
354
|
+
return Configuration.load(options.fetch(:config)) unless options.fetch(:treat_all_as_inline_rbs)
|
|
355
|
+
|
|
356
|
+
path = options.fetch(:config) || Configuration.discover
|
|
357
|
+
data = path && File.exist?(path) ? Configuration.load_with_includes(path) : {}
|
|
358
|
+
data = data.dup
|
|
359
|
+
data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
|
|
360
|
+
Configuration.new(Configuration::DEFAULTS.merge(data))
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def inject_treat_all_as_inline_rbs(entries)
|
|
364
|
+
filtered = entries.reject { |entry| rigor_rbs_inline_entry?(entry) }
|
|
365
|
+
filtered + [{
|
|
366
|
+
"gem" => "rigor-rbs-inline",
|
|
367
|
+
"id" => "rbs-inline",
|
|
368
|
+
"config" => { "require_magic_comment" => false }
|
|
369
|
+
}]
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def rigor_rbs_inline_entry?(entry)
|
|
373
|
+
case entry
|
|
374
|
+
when String
|
|
375
|
+
entry == "rigor-rbs-inline"
|
|
376
|
+
when Hash
|
|
377
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
378
|
+
string_keyed["gem"] == "rigor-rbs-inline" || string_keyed["id"] == "rbs-inline"
|
|
379
|
+
else
|
|
380
|
+
false
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
327
384
|
def handle_clear_cache(cache_root)
|
|
328
385
|
if File.directory?(cache_root)
|
|
329
386
|
FileUtils.rm_rf(cache_root)
|
|
@@ -516,6 +573,12 @@ module Rigor
|
|
|
516
573
|
LspCommand.new(argv: @argv, out: @out, err: @err).run
|
|
517
574
|
end
|
|
518
575
|
|
|
576
|
+
def run_mcp
|
|
577
|
+
require_relative "cli/mcp_command"
|
|
578
|
+
|
|
579
|
+
McpCommand.new(argv: @argv, out: @out, err: @err).run
|
|
580
|
+
end
|
|
581
|
+
|
|
519
582
|
def run_baseline
|
|
520
583
|
require_relative "cli/baseline_command"
|
|
521
584
|
|
|
@@ -528,6 +591,23 @@ module Rigor
|
|
|
528
591
|
CLI::TriageCommand.new(argv: @argv, out: @out, err: @err).run
|
|
529
592
|
end
|
|
530
593
|
|
|
594
|
+
def run_coverage
|
|
595
|
+
require_relative "cli/coverage_command"
|
|
596
|
+
|
|
597
|
+
CLI::CoverageCommand.new(argv: @argv, out: @out, err: @err).run
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def run_playground
|
|
601
|
+
begin
|
|
602
|
+
require "rigor/playground"
|
|
603
|
+
rescue LoadError
|
|
604
|
+
@err.puts "rigor playground requires the rigor-playground gem."
|
|
605
|
+
@err.puts "Install it with: gem install rigor-playground"
|
|
606
|
+
return EXIT_USAGE
|
|
607
|
+
end
|
|
608
|
+
Rigor::CLI::PlaygroundCommand.new(@argv[1..], @out, @err).run
|
|
609
|
+
end
|
|
610
|
+
|
|
531
611
|
def write_result(result, format)
|
|
532
612
|
case format
|
|
533
613
|
when "json"
|
|
@@ -570,7 +650,10 @@ module Rigor
|
|
|
570
650
|
diff Compare current diagnostics to a saved baseline JSON
|
|
571
651
|
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
572
652
|
lsp Run the Rigor Language Server (LSP) over stdio
|
|
653
|
+
mcp Run the Rigor MCP server over stdio (ADR-33)
|
|
573
654
|
triage Summarise diagnostics: distribution, hotspots, hints (ADR-23)
|
|
655
|
+
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
656
|
+
playground Start the browser playground (requires rigor-playground gem)
|
|
574
657
|
version Print the Rigor version
|
|
575
658
|
help Print this help
|
|
576
659
|
HELP
|
|
@@ -56,7 +56,7 @@ module Rigor
|
|
|
56
56
|
# run. The gem stubs are intentionally read-only and
|
|
57
57
|
# appended LAST so user-supplied `signature_paths` win on
|
|
58
58
|
# name conflicts.
|
|
59
|
-
def build_env_for(libraries:, signature_paths:)
|
|
59
|
+
def build_env_for(libraries:, signature_paths:, virtual_rbs: [])
|
|
60
60
|
rbs_loader = RBS::EnvironmentLoader.new
|
|
61
61
|
libraries.each do |library|
|
|
62
62
|
next unless rbs_loader.has_library?(library: library, version: nil)
|
|
@@ -70,7 +70,36 @@ module Rigor
|
|
|
70
70
|
vendored_gem_sig_paths.each do |path|
|
|
71
71
|
rbs_loader.add(path: path) if path.directory?
|
|
72
72
|
end
|
|
73
|
-
RBS::Environment.from_loader(rbs_loader)
|
|
73
|
+
env = RBS::Environment.from_loader(rbs_loader)
|
|
74
|
+
add_virtual_rbs(env, virtual_rbs)
|
|
75
|
+
env.resolve_type_names
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ADR-32 WD4 — merge synthesised-from-source RBS strings
|
|
79
|
+
# into the freshly-built environment. Each entry is a
|
|
80
|
+
# `[virtual_filename, rbs_source]` pair. `virtual_filename`
|
|
81
|
+
# is purely for diagnostic provenance (RBS parse errors
|
|
82
|
+
# cite it) — it is not a real file path. Per WD6 the
|
|
83
|
+
# synthesizer-emit path is responsible for catching its
|
|
84
|
+
# own parse errors and returning `nil` rather than
|
|
85
|
+
# garbage; this method assumes its input is parseable
|
|
86
|
+
# and only rescues `RBS::ParsingError` as a fail-soft.
|
|
87
|
+
def add_virtual_rbs(env, virtual_rbs)
|
|
88
|
+
return if virtual_rbs.nil? || virtual_rbs.empty?
|
|
89
|
+
|
|
90
|
+
virtual_rbs.each do |filename, content|
|
|
91
|
+
next if content.nil? || content.empty?
|
|
92
|
+
|
|
93
|
+
buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
|
|
94
|
+
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
95
|
+
source = ::RBS::Source::RBS.new(buffer, directives || [], decls || [])
|
|
96
|
+
env.add_source(source)
|
|
97
|
+
rescue ::RBS::BaseError
|
|
98
|
+
# WD6 fail-soft: a single broken virtual RBS contribution
|
|
99
|
+
# does not pull the whole env down. The plugin layer
|
|
100
|
+
# records a `source-rbs-synthesis-failed` info diagnostic
|
|
101
|
+
# in slice 2; here we just skip the entry.
|
|
102
|
+
end
|
|
74
103
|
end
|
|
75
104
|
|
|
76
105
|
# Per-gem `data/vendored_gem_sigs/<gem>/` directories that
|
|
@@ -95,7 +124,7 @@ module Rigor
|
|
|
95
124
|
end
|
|
96
125
|
end
|
|
97
126
|
|
|
98
|
-
attr_reader :libraries, :signature_paths, :cache_store
|
|
127
|
+
attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
|
|
99
128
|
|
|
100
129
|
# @param libraries [Array<String, Symbol>] stdlib library names to
|
|
101
130
|
# load on top of core (e.g., `["pathname", "json"]`). Empty by
|
|
@@ -114,10 +143,18 @@ module Rigor
|
|
|
114
143
|
# reflection artefacts). Pass `nil` (the default) to skip
|
|
115
144
|
# the cache entirely; the runner threads its own Store
|
|
116
145
|
# through here when caching is enabled.
|
|
117
|
-
|
|
146
|
+
# @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 —
|
|
147
|
+
# `[virtual_filename, rbs_source]` pairs synthesised from
|
|
148
|
+
# project source by a plugin's
|
|
149
|
+
# `Manifest#source_rbs_synthesizer`. Merged into the env
|
|
150
|
+
# after `signature_paths:` and the vendored stubs. Pass
|
|
151
|
+
# `[]` (the default) when no synthesizer-emitting plugin
|
|
152
|
+
# is loaded.
|
|
153
|
+
def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [])
|
|
118
154
|
@libraries = libraries.map(&:to_s).freeze
|
|
119
155
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
120
156
|
@cache_store = cache_store
|
|
157
|
+
@virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
|
|
121
158
|
# Per-loader memoization bucket. Held as a single
|
|
122
159
|
# mutable Hash so the loader instance itself can be
|
|
123
160
|
# `.freeze`d (per ADR-15 reflection-facade contract)
|
|
@@ -642,7 +679,11 @@ module Rigor
|
|
|
642
679
|
end
|
|
643
680
|
|
|
644
681
|
def build_env
|
|
645
|
-
self.class.build_env_for(
|
|
682
|
+
self.class.build_env_for(
|
|
683
|
+
libraries: @libraries,
|
|
684
|
+
signature_paths: @signature_paths,
|
|
685
|
+
virtual_rbs: @virtual_rbs
|
|
686
|
+
)
|
|
646
687
|
end
|
|
647
688
|
|
|
648
689
|
def build_instance_definition(class_name)
|
|
@@ -29,11 +29,12 @@ module Rigor
|
|
|
29
29
|
# return nil, and the consumer sites short-circuit on
|
|
30
30
|
# `reporter.nil?`.
|
|
31
31
|
class Reporters
|
|
32
|
-
attr_accessor :rbs_extended, :boundary_cross
|
|
32
|
+
attr_accessor :rbs_extended, :boundary_cross, :source_rbs_synthesis
|
|
33
33
|
|
|
34
|
-
def initialize(rbs_extended: nil, boundary_cross: nil)
|
|
34
|
+
def initialize(rbs_extended: nil, boundary_cross: nil, source_rbs_synthesis: nil)
|
|
35
35
|
@rbs_extended = rbs_extended
|
|
36
36
|
@boundary_cross = boundary_cross
|
|
37
|
+
@source_rbs_synthesis = source_rbs_synthesis
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
end
|