rigortype 0.1.15 → 0.1.16
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 +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +25 -1
- data/lib/rigor/analysis/diagnostic.rb +40 -0
- data/lib/rigor/analysis/runner.rb +61 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +135 -5
- data/lib/rigor/environment/rbs_loader.rb +259 -1
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/expression_typer.rb +9 -2
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
- data/lib/rigor/inference/method_dispatcher.rb +57 -10
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +127 -8
- data/lib/rigor/inference/statement_evaluator.rb +13 -8
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +321 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +10 -0
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +6 -1
- data/lib/rigor/type/union.rb +65 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +52 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fb4015697adf7cbc9d65b2ca6ff954c8cac5f784ffb9616153d4af8d2505ccc
|
|
4
|
+
data.tar.gz: 52ee5acb7d233c0e86c8cdca45151c5f745b4c13d7155fcb15fafdaaed1d6dc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f4b64afe3dbca26f6224762a2f1a3dd0bd4cade2ea9a61a30e2fba28ed29d3bb6d68bdfa73fb450536e5c7fef45e07a10a6fd0cec693ad228a828ff30905f8e
|
|
7
|
+
data.tar.gz: 4b5faa0d8c7295067f5a0f3d335c1800f22659e47b0428942405b07c1d38649163090f3a461e942a82aecaab6172e774ef88ca44f9a569b1392cdeee0be04086
|
data/README.md
CHANGED
|
@@ -265,10 +265,12 @@ In-source suppression: `# rigor:disable <rule>` silences a single line;
|
|
|
265
265
|
|
|
266
266
|
## Status
|
|
267
267
|
|
|
268
|
-
Current released version: **`v0.1.
|
|
268
|
+
Current released version: **`v0.1.15`** (2026-05-29). The analyzer is
|
|
269
269
|
usable on real Ruby code today; the rule catalogue is deliberately
|
|
270
270
|
conservative — Rigor's stance is to surface zero false positives while
|
|
271
|
-
the inference surface stabilises.
|
|
271
|
+
the inference surface stabilises. The `0.1.x` preview line has been
|
|
272
|
+
hardened against real OSS Rails codebases (Mastodon / Redmine / GitLab
|
|
273
|
+
FOSS); `v0.2.0` will open the first evaluation release.
|
|
272
274
|
|
|
273
275
|
Release history: [`CHANGELOG.md`](CHANGELOG.md). Forward-looking
|
|
274
276
|
commitments: [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
|
data/exe/rigor
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
# ADR-39 slice 5 — the `ruby_box` plugin-isolation strategy needs the
|
|
5
|
+
# experimental `Ruby::Box` feature, which is a process-start flag
|
|
6
|
+
# (`RUBY_BOX=1`, not toggleable at runtime). When a project selects that
|
|
7
|
+
# strategy (`RIGOR_PLUGIN_ISOLATION=ruby_box`, or the legacy `RIGOR_BOX`
|
|
8
|
+
# alias) we re-exec the same command with the flag set before anything
|
|
9
|
+
# else loads. The `RUBY_BOX` guard prevents an infinite re-exec, and the
|
|
10
|
+
# inherited environment (RUBYOPT / BUNDLE_*) preserves the Bundler
|
|
11
|
+
# context across the exec. The `none` (default) and `process` strategies
|
|
12
|
+
# need no flag, so this is a no-op for them — behaviour is unchanged
|
|
13
|
+
# unless a project opts into `ruby_box`.
|
|
14
|
+
rigor_isolation = ENV["RIGOR_PLUGIN_ISOLATION"].to_s
|
|
15
|
+
rigor_wants_box = rigor_isolation == "ruby_box" || !ENV["RIGOR_BOX"].to_s.empty?
|
|
16
|
+
if rigor_wants_box && ENV["RUBY_BOX"].to_s.empty?
|
|
17
|
+
require "rbconfig"
|
|
18
|
+
ENV["RUBY_BOX"] = "1"
|
|
19
|
+
ENV["RIGOR_PLUGIN_ISOLATION"] = "ruby_box" if rigor_isolation.empty?
|
|
20
|
+
exec(RbConfig.ruby, __FILE__, *ARGV)
|
|
21
|
+
end
|
|
22
|
+
|
|
4
23
|
lib = File.expand_path("../lib", __dir__)
|
|
5
24
|
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
|
6
25
|
|
|
@@ -383,7 +383,16 @@ module Rigor
|
|
|
383
383
|
# its model). Flagging an undefined method on a class
|
|
384
384
|
# with an open dynamic surface is unsound, so the rule
|
|
385
385
|
# skips it.
|
|
386
|
-
|
|
386
|
+
# An unbounded receiver surface: either a plugin-declared
|
|
387
|
+
# open receiver (ADR-26 — e.g. `ActiveRecord::Relation`), or
|
|
388
|
+
# a type Rigor synthesized (a missing-namespace module / a
|
|
389
|
+
# stub for a referenced-but-undeclared type) to keep a
|
|
390
|
+
# malformed project signature buildable. A synthesized stub's
|
|
391
|
+
# method table is empty only because Rigor invented it, not
|
|
392
|
+
# because the real type is empty (the real `DRb` has
|
|
393
|
+
# `start_service`), so enumerating it to prove a call
|
|
394
|
+
# "undefined" would be a false positive.
|
|
395
|
+
return nil if unbounded_receiver_surface?(class_name, scope)
|
|
387
396
|
|
|
388
397
|
# Slice 7 phase 12 — suppress when the user has
|
|
389
398
|
# declared the method in source (`def` /
|
|
@@ -566,6 +575,14 @@ module Rigor
|
|
|
566
575
|
# loaded plugin (manifest `open_receivers:`). An open
|
|
567
576
|
# class responds beyond its RBS surface, so the
|
|
568
577
|
# `call.undefined-method` rule must not fire for it.
|
|
578
|
+
# True when the receiver class responds beyond an enumerable
|
|
579
|
+
# RBS method table, so proving a call "undefined" against it is
|
|
580
|
+
# unsound: a plugin-declared open receiver, or a Rigor-
|
|
581
|
+
# synthesized stub type (see `RbsLoader#synthesized_type_names`).
|
|
582
|
+
def unbounded_receiver_surface?(class_name, scope)
|
|
583
|
+
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope)
|
|
584
|
+
end
|
|
585
|
+
|
|
569
586
|
def open_receiver?(class_name, scope)
|
|
570
587
|
registry = scope.environment&.plugin_registry
|
|
571
588
|
return false if registry.nil?
|
|
@@ -573,6 +590,13 @@ module Rigor
|
|
|
573
590
|
registry.open_receiver?(class_name)
|
|
574
591
|
end
|
|
575
592
|
|
|
593
|
+
def synthesized_stub_receiver?(class_name, scope)
|
|
594
|
+
loader = scope.environment&.rbs_loader
|
|
595
|
+
return false if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
596
|
+
|
|
597
|
+
loader.synthesized_type_names.include?(class_name.to_s.sub(/\A::/, ""))
|
|
598
|
+
end
|
|
599
|
+
|
|
576
600
|
def definition_available?(receiver_type, class_name, scope)
|
|
577
601
|
if receiver_type.is_a?(Type::Singleton)
|
|
578
602
|
!Rigor::Reflection.singleton_definition(class_name, scope: scope).nil?
|
|
@@ -59,6 +59,46 @@ module Rigor
|
|
|
59
59
|
@project_definition_site = project_definition_site
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Builds a Diagnostic positioned at a Prism node. Internalises
|
|
63
|
+
# the load-bearing convention every caller otherwise repeats:
|
|
64
|
+
# the line is the node's 1-based `start_line` and the column is
|
|
65
|
+
# `start_column + 1` (Prism columns are 0-based; Rigor reports
|
|
66
|
+
# 1-based). Pass any node responding to `#location`; all other
|
|
67
|
+
# fields forward to `#initialize` unchanged.
|
|
68
|
+
#
|
|
69
|
+
# `Plugin::Base#diagnostic` wraps this for plugin authors (who
|
|
70
|
+
# must not set `source_family` — the runner stamps it); core
|
|
71
|
+
# rules and other producers call it directly.
|
|
72
|
+
def self.from_node(node, path:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
73
|
+
source_family: DEFAULT_SOURCE_FAMILY,
|
|
74
|
+
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
75
|
+
from_location(
|
|
76
|
+
node.location, path: path, message: message, severity: severity, rule: rule,
|
|
77
|
+
source_family: source_family, receiver_type: receiver_type,
|
|
78
|
+
method_name: method_name, project_definition_site: project_definition_site
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Builds a Diagnostic from an explicit Prism location, applying
|
|
83
|
+
# the same 1-based `line` / `start_column + 1` convention as
|
|
84
|
+
# {.from_node}. Use this when the diagnostic should point at a
|
|
85
|
+
# *sub-location* rather than the whole node — most often a call's
|
|
86
|
+
# `message_loc` (the matcher / method name) instead of the
|
|
87
|
+
# receiver-spanning `node.location`. {.from_node} is sugar for
|
|
88
|
+
# `from_location(node.location, …)`.
|
|
89
|
+
def self.from_location(location, path:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
90
|
+
source_family: DEFAULT_SOURCE_FAMILY,
|
|
91
|
+
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
92
|
+
new(
|
|
93
|
+
path: path,
|
|
94
|
+
line: location.start_line,
|
|
95
|
+
column: location.start_column + 1,
|
|
96
|
+
message: message, severity: severity, rule: rule, source_family: source_family,
|
|
97
|
+
receiver_type: receiver_type, method_name: method_name,
|
|
98
|
+
project_definition_site: project_definition_site
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
62
102
|
def error?
|
|
63
103
|
severity == :error
|
|
64
104
|
end
|
|
@@ -106,6 +106,7 @@ module Rigor
|
|
|
106
106
|
# nil-guards.
|
|
107
107
|
@class_decl_paths_snapshot = {}.freeze
|
|
108
108
|
@signature_paths_snapshot = [].freeze
|
|
109
|
+
@synthesized_namespaces_snapshot = [].freeze
|
|
109
110
|
@cached_plugin_prepare_diagnostics = [].freeze
|
|
110
111
|
@project_discovered_classes = {}.freeze
|
|
111
112
|
@project_discovered_def_nodes = {}.freeze
|
|
@@ -113,6 +114,7 @@ module Rigor
|
|
|
113
114
|
@project_discovered_superclasses = {}.freeze
|
|
114
115
|
@project_discovered_includes = {}.freeze
|
|
115
116
|
@project_discovered_method_visibilities = {}.freeze
|
|
117
|
+
@project_discovered_methods = {}.freeze
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
# ADR-pending editor mode — present when the runner is wired
|
|
@@ -141,6 +143,7 @@ module Rigor
|
|
|
141
143
|
expansion = expand_paths(paths)
|
|
142
144
|
@class_decl_paths_snapshot = {}.freeze
|
|
143
145
|
@signature_paths_snapshot = []
|
|
146
|
+
@synthesized_namespaces_snapshot = []
|
|
144
147
|
|
|
145
148
|
if @prebuilt
|
|
146
149
|
adopt_prebuilt_project_scan(@prebuilt)
|
|
@@ -150,6 +153,7 @@ module Rigor
|
|
|
150
153
|
|
|
151
154
|
diagnostics = pre_file_diagnostics(expansion)
|
|
152
155
|
diagnostics += analyze_files(target_files(expansion))
|
|
156
|
+
diagnostics += rbs_synthesized_namespace_diagnostics
|
|
153
157
|
diagnostics += rbs_extended_reporter_diagnostics
|
|
154
158
|
diagnostics += boundary_cross_diagnostics
|
|
155
159
|
diagnostics += source_rbs_synthesis_diagnostics
|
|
@@ -263,6 +267,7 @@ module Rigor
|
|
|
263
267
|
@project_discovered_superclasses = def_index.fetch(:superclasses)
|
|
264
268
|
@project_discovered_includes = def_index.fetch(:includes)
|
|
265
269
|
@project_discovered_method_visibilities = def_index.fetch(:method_visibilities)
|
|
270
|
+
@project_discovered_methods = def_index.fetch(:methods)
|
|
266
271
|
end
|
|
267
272
|
|
|
268
273
|
# Internal: adopts a frozen {ProjectScan} snapshot supplied
|
|
@@ -303,6 +308,16 @@ module Rigor
|
|
|
303
308
|
dispatch_pool(files)
|
|
304
309
|
else
|
|
305
310
|
environment = resolve_sequential_environment(source_files: files)
|
|
311
|
+
# Snapshot the small synthesized-namespace name list (NOT the
|
|
312
|
+
# env — see the method comment) so #run can surface the
|
|
313
|
+
# malformed-RBS `:info` diagnostic without rebuilding the env.
|
|
314
|
+
# Gated on the project actually declaring `signature_paths:`:
|
|
315
|
+
# synthesis only matters for the project's own RBS, and
|
|
316
|
+
# `#synthesized_namespaces` forces the (otherwise-lazy) RBS env
|
|
317
|
+
# to build — doing so when there is no project sig set would
|
|
318
|
+
# warm `.rigor/cache` on a bare `--no-stats` run.
|
|
319
|
+
@synthesized_namespaces_snapshot =
|
|
320
|
+
project_signature_paths? ? (environment.rbs_loader&.synthesized_namespaces || []) : []
|
|
306
321
|
result = files.flat_map { |path| analyze_file(path, environment) }
|
|
307
322
|
if @collect_stats
|
|
308
323
|
loader = environment.rbs_loader
|
|
@@ -1116,6 +1131,48 @@ module Rigor
|
|
|
1116
1131
|
[build_rbs_coverage_missing_diagnostic(missing)]
|
|
1117
1132
|
end
|
|
1118
1133
|
|
|
1134
|
+
# Robustness uplift companion (ADR-5) — when the project's
|
|
1135
|
+
# `signature_paths:` RBS declared qualified names without their
|
|
1136
|
+
# enclosing namespace, `RbsLoader` synthesizes the missing
|
|
1137
|
+
# `module`s so the otherwise-inert signatures resolve. Surface a
|
|
1138
|
+
# single `:info` diagnostic naming them so the user knows their
|
|
1139
|
+
# sig set is malformed (`rbs validate` rejects it) and can fix it
|
|
1140
|
+
# at the source. Authored `:info`: the analysis already succeeded;
|
|
1141
|
+
# this is advisory, never a gate. Empty for a well-formed sig set.
|
|
1142
|
+
def rbs_synthesized_namespace_diagnostics
|
|
1143
|
+
synthesized = @synthesized_namespaces_snapshot
|
|
1144
|
+
return [] if synthesized.nil? || synthesized.empty?
|
|
1145
|
+
|
|
1146
|
+
[build_rbs_synthesized_namespace_diagnostic(synthesized)]
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
# True when the project declares its own `signature_paths:` (the
|
|
1150
|
+
# only place the qualified-name-without-namespace mistake lives).
|
|
1151
|
+
def project_signature_paths?
|
|
1152
|
+
paths = @configuration.signature_paths
|
|
1153
|
+
!(paths.nil? || paths.empty?)
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
def build_rbs_synthesized_namespace_diagnostic(synthesized)
|
|
1157
|
+
sample_size = 5
|
|
1158
|
+
sample = synthesized.first(sample_size)
|
|
1159
|
+
suffix = synthesized.size > sample_size ? ", and #{synthesized.size - sample_size} more" : ""
|
|
1160
|
+
Diagnostic.new(
|
|
1161
|
+
path: ".rigor.yml",
|
|
1162
|
+
line: 1,
|
|
1163
|
+
column: 1,
|
|
1164
|
+
message: "#{synthesized.size} RBS namespace(s) under `signature_paths:` are " \
|
|
1165
|
+
"referenced by qualified declarations (e.g. `class Foo::Bar`) but never " \
|
|
1166
|
+
"declared: #{sample.join(', ')}#{suffix}. `rbs validate` rejects this; " \
|
|
1167
|
+
"Rigor synthesized the missing `module`(s) so the signatures still " \
|
|
1168
|
+
"resolve. Declare each (`module <name>` / `class <name>`) in your RBS to " \
|
|
1169
|
+
"make the sig set valid upstream.",
|
|
1170
|
+
severity: :info,
|
|
1171
|
+
rule: "rbs.coverage.synthesized-namespace",
|
|
1172
|
+
source_family: :builtin
|
|
1173
|
+
)
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1119
1176
|
def build_rbs_coverage_missing_diagnostic(missing)
|
|
1120
1177
|
sample_size = 5
|
|
1121
1178
|
sample = missing.first(sample_size).map(&:gem_name)
|
|
@@ -1328,8 +1385,9 @@ module Rigor
|
|
|
1328
1385
|
end
|
|
1329
1386
|
|
|
1330
1387
|
def collect_plugin_diagnostics(plugin, path, root, scope)
|
|
1331
|
-
raw = plugin.diagnostics_for_file(path: path, scope: scope, root: root)
|
|
1332
|
-
|
|
1388
|
+
raw = Array(plugin.diagnostics_for_file(path: path, scope: scope, root: root))
|
|
1389
|
+
raw += plugin.node_rule_diagnostics(path: path, scope: scope, root: root)
|
|
1390
|
+
raw.map { |diagnostic| stamp_plugin_diagnostic(diagnostic, plugin.manifest.id) }
|
|
1333
1391
|
rescue StandardError => e
|
|
1334
1392
|
[plugin_runtime_error_diagnostic(path, plugin, e)]
|
|
1335
1393
|
end
|
|
@@ -1463,6 +1521,7 @@ module Rigor
|
|
|
1463
1521
|
unless @project_discovered_method_visibilities.empty?
|
|
1464
1522
|
scope = scope.with_discovered_method_visibilities(@project_discovered_method_visibilities)
|
|
1465
1523
|
end
|
|
1524
|
+
scope = scope.with_discovered_methods(@project_discovered_methods) unless @project_discovered_methods.empty?
|
|
1466
1525
|
scope
|
|
1467
1526
|
end
|
|
1468
1527
|
|
|
@@ -284,8 +284,9 @@ module Rigor
|
|
|
284
284
|
end
|
|
285
285
|
|
|
286
286
|
def collect_plugin_diagnostics(plugin, path, root, scope)
|
|
287
|
-
raw = plugin.diagnostics_for_file(path: path, scope: scope, root: root)
|
|
288
|
-
|
|
287
|
+
raw = Array(plugin.diagnostics_for_file(path: path, scope: scope, root: root))
|
|
288
|
+
raw += plugin.node_rule_diagnostics(path: path, scope: scope, root: root)
|
|
289
|
+
raw.map { |diagnostic| stamp_plugin_diagnostic(diagnostic, plugin.manifest.id) }
|
|
289
290
|
rescue StandardError => e
|
|
290
291
|
[plugin_runtime_error_diagnostic(path, plugin, e)]
|
|
291
292
|
end
|
|
@@ -26,8 +26,12 @@ module Rigor
|
|
|
26
26
|
# mixes this into the cache key, so a bump implicitly
|
|
27
27
|
# invalidates every cached value. v2 added the
|
|
28
28
|
# `dependencies` slot for ADR-10 per-gem-version cache slice
|
|
29
|
-
# invalidation.
|
|
30
|
-
|
|
29
|
+
# invalidation. v3: `RbsLoader.build_env_for` now synthesizes
|
|
30
|
+
# `module`s for namespaces a project's `signature_paths:` RBS
|
|
31
|
+
# references but never declares, so the marshalled RBS env
|
|
32
|
+
# cached by an older Rigor (which would leave those signatures
|
|
33
|
+
# inert) MUST be rebuilt for the synthesis to take effect.
|
|
34
|
+
SCHEMA_VERSION = 3
|
|
31
35
|
|
|
32
36
|
# Per-slot entry value objects. Constructors validate enums /
|
|
33
37
|
# required fields and freeze the resulting struct so no caller
|
|
@@ -32,7 +32,15 @@ module Rigor
|
|
|
32
32
|
# `trait_registries:` / `external_files:` /
|
|
33
33
|
# `type_node_resolvers:` / `hkt_registrations:` /
|
|
34
34
|
# `hkt_definitions:` / `protocol_contracts:` /
|
|
35
|
-
# `source_rbs_synthesizer:`)
|
|
35
|
+
# `source_rbs_synthesizer:`);
|
|
36
|
+
# - the ADR-37 narrow extension protocols read off the plugin
|
|
37
|
+
# class — `node_rule` node types, `dynamic_return` receivers,
|
|
38
|
+
# `type_specifier` methods.
|
|
39
|
+
#
|
|
40
|
+
# `--capabilities` switches to a focused catalogue of just the
|
|
41
|
+
# narrow-protocol gate values + produced/consumed facts (ADR-37
|
|
42
|
+
# § "Machine-readable capability catalogue") — the AI-legibility
|
|
43
|
+
# surface that lets an agent enumerate what every plugin does.
|
|
36
44
|
#
|
|
37
45
|
# Output formats: `text` (default, human-readable table) and
|
|
38
46
|
# `json` (for tooling — SKILLs, CI gates, editor integrations).
|
|
@@ -52,7 +60,7 @@ module Rigor
|
|
|
52
60
|
# the RBS environment without conflict (requires constructing
|
|
53
61
|
# the Environment, which is heavier than the loader-only
|
|
54
62
|
# pass this slice does).
|
|
55
|
-
class PluginsCommand
|
|
63
|
+
class PluginsCommand # rubocop:disable Metrics/ClassLength
|
|
56
64
|
USAGE = "Usage: rigor plugins [options]"
|
|
57
65
|
|
|
58
66
|
def initialize(argv:, out: $stdout, err: $stderr)
|
|
@@ -69,7 +77,7 @@ module Rigor
|
|
|
69
77
|
rows = build_rows(configuration)
|
|
70
78
|
|
|
71
79
|
renderer = PluginsRenderer.new(rows: rows, configuration_path: config_path)
|
|
72
|
-
@out.puts(
|
|
80
|
+
@out.puts(render(renderer, options))
|
|
73
81
|
|
|
74
82
|
any_load_errors = rows.any? { |row| row.fetch(:status) == :load_error }
|
|
75
83
|
return 1 if any_load_errors && options.fetch(:strict)
|
|
@@ -79,13 +87,32 @@ module Rigor
|
|
|
79
87
|
|
|
80
88
|
private
|
|
81
89
|
|
|
90
|
+
# Picks the renderer view. `--capabilities` switches to the
|
|
91
|
+
# focused extension-protocol catalogue (ADR-37 § "Machine-readable
|
|
92
|
+
# capability catalogue") — per plugin, only the gate values that
|
|
93
|
+
# tell a reader (or an AI agent) exactly what the plugin
|
|
94
|
+
# contributes: the node-rule node types, the dynamic-return
|
|
95
|
+
# receivers, the type-specifier methods, and the produced /
|
|
96
|
+
# consumed facts. The default view stays the full activation report.
|
|
97
|
+
def render(renderer, options)
|
|
98
|
+
json = options.fetch(:format) == "json"
|
|
99
|
+
if options.fetch(:capabilities)
|
|
100
|
+
json ? renderer.capabilities_json : renderer.capabilities_text
|
|
101
|
+
else
|
|
102
|
+
json ? renderer.json : renderer.text
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
82
106
|
def parse_options
|
|
83
|
-
options = { config: nil, format: "text", strict: false }
|
|
107
|
+
options = { config: nil, format: "text", strict: false, capabilities: false }
|
|
84
108
|
OptionParser.new do |opts|
|
|
85
109
|
opts.banner = USAGE
|
|
86
110
|
opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
|
|
87
111
|
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
88
112
|
opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
|
|
113
|
+
opts.on("--capabilities", "Emit the per-plugin extension-protocol catalogue (ADR-37)") do
|
|
114
|
+
options[:capabilities] = true
|
|
115
|
+
end
|
|
89
116
|
end.parse!(@argv)
|
|
90
117
|
validate!(options)
|
|
91
118
|
options
|
|
@@ -165,9 +192,25 @@ module Rigor
|
|
|
165
192
|
manifest = plugin.manifest
|
|
166
193
|
identity_fields(gem_name, manifest, config)
|
|
167
194
|
.merge(extension_fields(plugin, manifest))
|
|
195
|
+
.merge(narrow_protocol_fields(plugin))
|
|
168
196
|
.merge(load_error: nil)
|
|
169
197
|
end
|
|
170
198
|
|
|
199
|
+
# ADR-37 narrow extension protocols. Unlike the 10 declarative
|
|
200
|
+
# manifest fields, these are class-level DSLs (`node_rule` /
|
|
201
|
+
# `dynamic_return` / `type_specifier`), so they are read off the
|
|
202
|
+
# plugin class rather than the manifest. The gate values — node
|
|
203
|
+
# types, receiver class names, specified method names — are the
|
|
204
|
+
# greppable, enumerable surface the capability catalogue exposes.
|
|
205
|
+
def narrow_protocol_fields(plugin)
|
|
206
|
+
klass = plugin.class
|
|
207
|
+
{
|
|
208
|
+
node_rule_types: klass.node_rules.map { |r| r[:node_type].name }.uniq,
|
|
209
|
+
dynamic_return_receivers: klass.dynamic_returns.flat_map { |r| r[:receivers] }.uniq,
|
|
210
|
+
type_specifier_methods: klass.type_specifiers.flat_map { |r| r[:methods] }.map(&:to_s).uniq
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
171
214
|
def identity_fields(gem_name, manifest, config)
|
|
172
215
|
{
|
|
173
216
|
gem: gem_name,
|
|
@@ -225,6 +268,9 @@ module Rigor
|
|
|
225
268
|
hkt_definitions: 0,
|
|
226
269
|
protocol_contracts: 0,
|
|
227
270
|
source_rbs_synthesizer: false,
|
|
271
|
+
node_rule_types: [],
|
|
272
|
+
dynamic_return_receivers: [],
|
|
273
|
+
type_specifier_methods: [],
|
|
228
274
|
load_error: error&.message || "plugin did not register or could not be matched to a registered class"
|
|
229
275
|
}
|
|
230
276
|
end
|
|
@@ -299,6 +345,7 @@ module Rigor
|
|
|
299
345
|
external_files: 0, type_node_resolvers: 0,
|
|
300
346
|
hkt_registrations: 0, hkt_definitions: 0,
|
|
301
347
|
protocol_contracts: 0, source_rbs_synthesizer: false,
|
|
348
|
+
node_rule_types: [], dynamic_return_receivers: [], type_specifier_methods: [],
|
|
302
349
|
load_error: error.message
|
|
303
350
|
}
|
|
304
351
|
end
|
|
@@ -13,7 +13,7 @@ module Rigor
|
|
|
13
13
|
# tooling (SKILLs, CI, editor integrations) while text is
|
|
14
14
|
# for interactive inspection. Rows are printed in the order
|
|
15
15
|
# the loader resolved them.
|
|
16
|
-
class PluginsRenderer
|
|
16
|
+
class PluginsRenderer # rubocop:disable Metrics/ClassLength
|
|
17
17
|
def initialize(rows:, configuration_path:)
|
|
18
18
|
@rows = rows
|
|
19
19
|
@configuration_path = configuration_path
|
|
@@ -42,8 +42,74 @@ module Rigor
|
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# ADR-37 § "Machine-readable capability catalogue" — the focused
|
|
46
|
+
# per-plugin extension-protocol dump. Only loaded plugins appear
|
|
47
|
+
# (a plugin that failed to load contributes no capabilities), and
|
|
48
|
+
# each carries only the gate values an agent enumerates to learn
|
|
49
|
+
# what the plugin does: node-rule node types, dynamic-return
|
|
50
|
+
# receivers, type-specifier methods, and produced / consumed facts.
|
|
51
|
+
def capabilities_json
|
|
52
|
+
JSON.pretty_generate(
|
|
53
|
+
{
|
|
54
|
+
"configuration" => @configuration_path,
|
|
55
|
+
"capabilities" => loaded_rows.map { |row| capabilities_json_for(row) }
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def capabilities_text
|
|
61
|
+
lines = ["Plugin capability catalogue (ADR-37 narrow extension protocols)", ""]
|
|
62
|
+
loaded = loaded_rows
|
|
63
|
+
if loaded.empty?
|
|
64
|
+
lines << " (no plugins loaded)"
|
|
65
|
+
else
|
|
66
|
+
loaded.each_with_index do |row, index|
|
|
67
|
+
lines.concat(capability_lines(row))
|
|
68
|
+
lines << "" unless index == loaded.size - 1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
45
74
|
private
|
|
46
75
|
|
|
76
|
+
def loaded_rows
|
|
77
|
+
@rows.select { |r| r[:status] == :loaded }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def capability_lines(row)
|
|
81
|
+
lines = [" #{row[:id]} v#{row[:version]} (#{row[:gem]})"]
|
|
82
|
+
capability_surfaces(row).each { |surface| lines << " #{surface}" }
|
|
83
|
+
lines << " (no narrow extension protocols declared)" if lines.size == 1
|
|
84
|
+
lines
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The non-empty capability surfaces for a plugin, each as a
|
|
88
|
+
# `label: a, b, c` string. Data-driven so the catalogue stays a
|
|
89
|
+
# single source of truth shared between the text and JSON views.
|
|
90
|
+
def capability_surfaces(row)
|
|
91
|
+
[
|
|
92
|
+
["node_rule", row[:node_rule_types]],
|
|
93
|
+
["dynamic_return receivers", row[:dynamic_return_receivers]],
|
|
94
|
+
["type_specifier methods", row[:type_specifier_methods]],
|
|
95
|
+
["produces", row[:produces]],
|
|
96
|
+
["consumes", row[:consumes]]
|
|
97
|
+
].filter_map { |label, values| "#{label}: #{values.join(', ')}" if values.any? }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def capabilities_json_for(row)
|
|
101
|
+
{
|
|
102
|
+
"id" => row[:id],
|
|
103
|
+
"gem" => row[:gem],
|
|
104
|
+
"version" => row[:version],
|
|
105
|
+
"node_rule_types" => row[:node_rule_types],
|
|
106
|
+
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
107
|
+
"type_specifier_methods" => row[:type_specifier_methods],
|
|
108
|
+
"produces" => row[:produces],
|
|
109
|
+
"consumes" => row[:consumes]
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
47
113
|
def header
|
|
48
114
|
loaded = @rows.count { |r| r[:status] == :loaded }
|
|
49
115
|
errored = @rows.count { |r| r[:status] == :load_error }
|
|
@@ -99,6 +165,22 @@ module Rigor
|
|
|
99
165
|
lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
|
|
100
166
|
lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
|
|
101
167
|
lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
|
|
168
|
+
lines.concat(narrow_protocol_lines(row))
|
|
169
|
+
lines
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# ADR-37 narrow extension protocols (node_rule / dynamic_return /
|
|
173
|
+
# type_specifier). Surfaced in the full report alongside the
|
|
174
|
+
# declarative surfaces; `--capabilities` is the focused view.
|
|
175
|
+
def narrow_protocol_lines(row)
|
|
176
|
+
lines = []
|
|
177
|
+
lines << " node_rule: #{row[:node_rule_types].join(', ')}" if row[:node_rule_types].any?
|
|
178
|
+
if row[:dynamic_return_receivers].any?
|
|
179
|
+
lines << " dynamic_return receivers: #{row[:dynamic_return_receivers].join(', ')}"
|
|
180
|
+
end
|
|
181
|
+
if row[:type_specifier_methods].any?
|
|
182
|
+
lines << " type_specifier methods: #{row[:type_specifier_methods].join(', ')}"
|
|
183
|
+
end
|
|
102
184
|
lines
|
|
103
185
|
end
|
|
104
186
|
|
|
@@ -157,6 +239,9 @@ module Rigor
|
|
|
157
239
|
"hkt_definitions" => row[:hkt_definitions],
|
|
158
240
|
"protocol_contracts" => row[:protocol_contracts],
|
|
159
241
|
"source_rbs_synthesizer" => row[:source_rbs_synthesizer],
|
|
242
|
+
"node_rule_types" => row[:node_rule_types],
|
|
243
|
+
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
244
|
+
"type_specifier_methods" => row[:type_specifier_methods],
|
|
160
245
|
"load_error" => row[:load_error]
|
|
161
246
|
}
|
|
162
247
|
end
|