rigortype 0.1.14 → 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 +10 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +428 -6
- data/lib/rigor/analysis/diagnostic.rb +55 -3
- data/lib/rigor/analysis/rule_catalog.rb +80 -0
- data/lib/rigor/analysis/runner.rb +71 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugin_command.rb +245 -0
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +143 -5
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- 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 +184 -27
- 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/scope.rb +27 -1
- 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/triage/catalogue.rb +71 -5
- 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/scope.rbs +3 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +33 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +65 -26
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +213 -80
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- data/skills/rigor-project-init/SKILL.md +72 -7
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +233 -19
- metadata +53 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -288,6 +288,86 @@ module Rigor
|
|
|
288
288
|
since: "0.1.2"
|
|
289
289
|
),
|
|
290
290
|
|
|
291
|
+
CheckRules::RULE_OVERRIDE_VISIBILITY_REDUCED => Entry.new(
|
|
292
|
+
id: CheckRules::RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
293
|
+
summary: "Instance-method override reduces the visibility it inherits from an ancestor.",
|
|
294
|
+
fires_when: [
|
|
295
|
+
"An instance `def` shadows a same-name instance method defined by a project-discovered " \
|
|
296
|
+
"ancestor (included/prepended module or superclass, cross-file).",
|
|
297
|
+
"The override's source-discovered visibility is strictly more restrictive than the " \
|
|
298
|
+
"ancestor's (public → protected/private, or protected → private).",
|
|
299
|
+
"Both visibilities are statically observable from project source."
|
|
300
|
+
],
|
|
301
|
+
does_not_fire_when: [
|
|
302
|
+
"Override raises or preserves visibility (only reductions break substitutability).",
|
|
303
|
+
"The shadowed method lives on an RBS-known / third-party ancestor (RBS models only " \
|
|
304
|
+
"public/private; RBS-parent visibility is a deferred follow-on).",
|
|
305
|
+
"`def self.foo` singleton methods (visibility is instance-side only).",
|
|
306
|
+
"The `private def foo; end` wrap-around form (not yet tracked by the visibility walker)."
|
|
307
|
+
],
|
|
308
|
+
suppression: "`# rigor:disable def.override-visibility-reduced` on the override.",
|
|
309
|
+
severity_authored: :warning,
|
|
310
|
+
severity_by_profile: { lenient: :off, balanced: :warning, strict: :error },
|
|
311
|
+
since: "0.1.15"
|
|
312
|
+
),
|
|
313
|
+
|
|
314
|
+
CheckRules::RULE_OVERRIDE_RETURN_WIDENED => Entry.new(
|
|
315
|
+
id: CheckRules::RULE_OVERRIDE_RETURN_WIDENED,
|
|
316
|
+
summary: "Instance-method override widens the return type it inherits from an ancestor.",
|
|
317
|
+
fires_when: [
|
|
318
|
+
"An instance `def` with an authored RBS signature overrides a same-name method whose " \
|
|
319
|
+
"RBS signature is declared by a project-discovered ancestor (module or superclass).",
|
|
320
|
+
"The override's declared return is not acceptable where the ancestor's declared return " \
|
|
321
|
+
"is expected (`parent_return.accepts(override_return)` is `:no`) — a covariance violation."
|
|
322
|
+
],
|
|
323
|
+
does_not_fire_when: [
|
|
324
|
+
"Either side lacks an authored RBS signature (WD1 both-sides-authored gate).",
|
|
325
|
+
"The override narrows or preserves the return (covariant-safe).",
|
|
326
|
+
"The ancestor's return is `untyped` / `self` / an unbound generic (degrades to " \
|
|
327
|
+
"`Dynamic[Top]`, which accepts everything — FP-safe).",
|
|
328
|
+
"The subtype relationship between the two return types is not resolvable from loaded " \
|
|
329
|
+
"Ruby classes / their ancestors (a user-only class hierarchy degrades to `:maybe` and " \
|
|
330
|
+
"stays silent — the check has reach over core / stdlib / loadable-gem hierarchies).",
|
|
331
|
+
"`def self.foo` singleton methods (instance-side only in v1).",
|
|
332
|
+
"The shadowed method lives only on an RBS-known / third-party ancestor not in the " \
|
|
333
|
+
"project-discovered chain (user-source ancestor scope in v1)."
|
|
334
|
+
],
|
|
335
|
+
suppression: "`# rigor:disable def.override-return-widened` on the override.",
|
|
336
|
+
severity_authored: :warning,
|
|
337
|
+
severity_by_profile: { lenient: :off, balanced: :warning, strict: :error },
|
|
338
|
+
since: "0.1.15"
|
|
339
|
+
),
|
|
340
|
+
|
|
341
|
+
CheckRules::RULE_OVERRIDE_PARAM_NARROWED => Entry.new(
|
|
342
|
+
id: CheckRules::RULE_OVERRIDE_PARAM_NARROWED,
|
|
343
|
+
summary: "Instance-method override narrows a parameter type it inherits from an ancestor.",
|
|
344
|
+
fires_when: [
|
|
345
|
+
"An instance `def` with an authored RBS signature overrides a same-name method whose " \
|
|
346
|
+
"RBS signature is declared by a project-discovered ancestor (module or superclass).",
|
|
347
|
+
"At some matching positional parameter index, the override's slot cannot accept the " \
|
|
348
|
+
"ancestor's parameter type (`override_param.accepts(parent_param)` is `:no`) — a " \
|
|
349
|
+
"contravariance violation (the override narrowed the parameter)."
|
|
350
|
+
],
|
|
351
|
+
does_not_fire_when: [
|
|
352
|
+
"Either side lacks an authored RBS signature (WD1 both-sides-authored gate).",
|
|
353
|
+
"The override widens or preserves the parameter (contravariant-safe).",
|
|
354
|
+
"Either side is overloaded (more than one method type — arm mapping is ambiguous).",
|
|
355
|
+
"The ancestor's parameter is `untyped` / an unbound generic / an interface (degrades " \
|
|
356
|
+
"to `Dynamic[Top]`, which is passable to anything — FP-safe).",
|
|
357
|
+
"The subtype relationship between the two parameter types is not resolvable from loaded " \
|
|
358
|
+
"Ruby classes / their ancestors (a user-only class hierarchy degrades to `:maybe` and " \
|
|
359
|
+
"stays silent — the check has reach over core / stdlib / loadable-gem hierarchies).",
|
|
360
|
+
"Arity / keyword-requiredness divergence (out of scope for v1 — positional types only).",
|
|
361
|
+
"`def self.foo` singleton methods (instance-side only in v1).",
|
|
362
|
+
"The shadowed method lives only on an RBS-known / third-party ancestor (user-source " \
|
|
363
|
+
"ancestor scope in v1)."
|
|
364
|
+
],
|
|
365
|
+
suppression: "`# rigor:disable def.override-param-narrowed` on the override.",
|
|
366
|
+
severity_authored: :warning,
|
|
367
|
+
severity_by_profile: { lenient: :off, balanced: :warning, strict: :error },
|
|
368
|
+
since: "0.1.15"
|
|
369
|
+
),
|
|
370
|
+
|
|
291
371
|
CheckRules::RULE_IVAR_WRITE_MISMATCH => Entry.new(
|
|
292
372
|
id: CheckRules::RULE_IVAR_WRITE_MISMATCH,
|
|
293
373
|
summary: "Same instance variable assigned a different concrete class within one class.",
|
|
@@ -106,11 +106,15 @@ 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
|
+
@project_discovered_def_sources = {}.freeze
|
|
112
114
|
@project_discovered_superclasses = {}.freeze
|
|
113
115
|
@project_discovered_includes = {}.freeze
|
|
116
|
+
@project_discovered_method_visibilities = {}.freeze
|
|
117
|
+
@project_discovered_methods = {}.freeze
|
|
114
118
|
end
|
|
115
119
|
|
|
116
120
|
# ADR-pending editor mode — present when the runner is wired
|
|
@@ -139,6 +143,7 @@ module Rigor
|
|
|
139
143
|
expansion = expand_paths(paths)
|
|
140
144
|
@class_decl_paths_snapshot = {}.freeze
|
|
141
145
|
@signature_paths_snapshot = []
|
|
146
|
+
@synthesized_namespaces_snapshot = []
|
|
142
147
|
|
|
143
148
|
if @prebuilt
|
|
144
149
|
adopt_prebuilt_project_scan(@prebuilt)
|
|
@@ -148,6 +153,7 @@ module Rigor
|
|
|
148
153
|
|
|
149
154
|
diagnostics = pre_file_diagnostics(expansion)
|
|
150
155
|
diagnostics += analyze_files(target_files(expansion))
|
|
156
|
+
diagnostics += rbs_synthesized_namespace_diagnostics
|
|
151
157
|
diagnostics += rbs_extended_reporter_diagnostics
|
|
152
158
|
diagnostics += boundary_cross_diagnostics
|
|
153
159
|
diagnostics += source_rbs_synthesis_diagnostics
|
|
@@ -257,8 +263,11 @@ module Rigor
|
|
|
257
263
|
def_index =
|
|
258
264
|
Inference::ScopeIndexer.discovered_def_index_for_paths(expansion.fetch(:files), buffer: @buffer)
|
|
259
265
|
@project_discovered_def_nodes = def_index.fetch(:def_nodes)
|
|
266
|
+
@project_discovered_def_sources = def_index.fetch(:def_sources)
|
|
260
267
|
@project_discovered_superclasses = def_index.fetch(:superclasses)
|
|
261
268
|
@project_discovered_includes = def_index.fetch(:includes)
|
|
269
|
+
@project_discovered_method_visibilities = def_index.fetch(:method_visibilities)
|
|
270
|
+
@project_discovered_methods = def_index.fetch(:methods)
|
|
262
271
|
end
|
|
263
272
|
|
|
264
273
|
# Internal: adopts a frozen {ProjectScan} snapshot supplied
|
|
@@ -299,6 +308,16 @@ module Rigor
|
|
|
299
308
|
dispatch_pool(files)
|
|
300
309
|
else
|
|
301
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 || []) : []
|
|
302
321
|
result = files.flat_map { |path| analyze_file(path, environment) }
|
|
303
322
|
if @collect_stats
|
|
304
323
|
loader = environment.rbs_loader
|
|
@@ -1112,6 +1131,48 @@ module Rigor
|
|
|
1112
1131
|
[build_rbs_coverage_missing_diagnostic(missing)]
|
|
1113
1132
|
end
|
|
1114
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
|
+
|
|
1115
1176
|
def build_rbs_coverage_missing_diagnostic(missing)
|
|
1116
1177
|
sample_size = 5
|
|
1117
1178
|
sample = missing.first(sample_size).map(&:gem_name)
|
|
@@ -1324,8 +1385,9 @@ module Rigor
|
|
|
1324
1385
|
end
|
|
1325
1386
|
|
|
1326
1387
|
def collect_plugin_diagnostics(plugin, path, root, scope)
|
|
1327
|
-
raw = plugin.diagnostics_for_file(path: path, scope: scope, root: root)
|
|
1328
|
-
|
|
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) }
|
|
1329
1391
|
rescue StandardError => e
|
|
1330
1392
|
[plugin_runtime_error_diagnostic(path, plugin, e)]
|
|
1331
1393
|
end
|
|
@@ -1449,10 +1511,17 @@ module Rigor
|
|
|
1449
1511
|
unless @project_discovered_def_nodes.empty?
|
|
1450
1512
|
scope = scope.with_discovered_def_nodes(@project_discovered_def_nodes)
|
|
1451
1513
|
end
|
|
1514
|
+
unless @project_discovered_def_sources.empty?
|
|
1515
|
+
scope = scope.with_discovered_def_sources(@project_discovered_def_sources)
|
|
1516
|
+
end
|
|
1452
1517
|
unless @project_discovered_superclasses.empty?
|
|
1453
1518
|
scope = scope.with_discovered_superclasses(@project_discovered_superclasses)
|
|
1454
1519
|
end
|
|
1455
1520
|
scope = scope.with_discovered_includes(@project_discovered_includes) unless @project_discovered_includes.empty?
|
|
1521
|
+
unless @project_discovered_method_visibilities.empty?
|
|
1522
|
+
scope = scope.with_discovered_method_visibilities(@project_discovered_method_visibilities)
|
|
1523
|
+
end
|
|
1524
|
+
scope = scope.with_discovered_methods(@project_discovered_methods) unless @project_discovered_methods.empty?
|
|
1456
1525
|
scope
|
|
1457
1526
|
end
|
|
1458
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
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# `rigor plugin` (singular) — discover and read the plugin source
|
|
6
|
+
# bundled with the `rigortype` gem.
|
|
7
|
+
#
|
|
8
|
+
# Rigor ships ~30 production plugins under `plugins/` and a set of
|
|
9
|
+
# tutorial plugins under `examples/`. When Rigor is installed via
|
|
10
|
+
# `mise` / `gem install` the gem checkout is on disk, so a plugin
|
|
11
|
+
# author (or an AI coding agent following the `rigor-plugin-author`
|
|
12
|
+
# skill) can read a real, working plugin as a worked example —
|
|
13
|
+
# instead of guessing the `Rigor::Plugin::Base` surface from prose.
|
|
14
|
+
# This command outputs the absolute paths so they can be found and
|
|
15
|
+
# read regardless of where the gem landed.
|
|
16
|
+
#
|
|
17
|
+
# It is deliberately distinct from `rigor plugins` (plural), which
|
|
18
|
+
# reports the activation status of the plugins configured in *your*
|
|
19
|
+
# `.rigor.yml`. This command (singular) browses the plugins bundled
|
|
20
|
+
# in the *toolchain*. Mnemonic: "plugins" = my config; "plugin" =
|
|
21
|
+
# the catalogue I can learn from.
|
|
22
|
+
#
|
|
23
|
+
# Subcommands:
|
|
24
|
+
#
|
|
25
|
+
# - `rigor plugin list` — every bundled plugin + example,
|
|
26
|
+
# name + absolute directory path.
|
|
27
|
+
# - `rigor plugin path <name>` — one-line absolute path to the
|
|
28
|
+
# plugin's directory (Read-tool input).
|
|
29
|
+
# - `rigor plugin print <name>` — a header (dir / lib / sig / README
|
|
30
|
+
# paths) followed by the plugin's main
|
|
31
|
+
# `lib/<name>.rb` source body.
|
|
32
|
+
# - `rigor plugin root` — the rigortype gem root and its key
|
|
33
|
+
# subdirectories (lib/, plugins/,
|
|
34
|
+
# examples/, skills/, sig/), so an
|
|
35
|
+
# author can read the public plugin
|
|
36
|
+
# API (`lib/rigor/plugin.rb`) directly.
|
|
37
|
+
#
|
|
38
|
+
# `rigor plugin` with no subcommand is an alias for `list`.
|
|
39
|
+
#
|
|
40
|
+
# **Docker / cross-filesystem note.** Every path printed is resolved
|
|
41
|
+
# at runtime from this file's location, so it is correct *on the
|
|
42
|
+
# filesystem where `rigor` runs*. If you run `rigor` inside a
|
|
43
|
+
# container but read files from the host (or vice versa), the paths
|
|
44
|
+
# will not resolve — read them from the same environment that ran
|
|
45
|
+
# the command (`rigor plugin print` inlines the body for exactly
|
|
46
|
+
# this case: it works with no file-reading tool at all).
|
|
47
|
+
class PluginCommand
|
|
48
|
+
USAGE = <<~USAGE
|
|
49
|
+
Usage: rigor plugin <subcommand> [args]
|
|
50
|
+
|
|
51
|
+
Browse the plugins bundled in the rigortype toolchain (worked
|
|
52
|
+
examples for authoring your own). For the activation status of
|
|
53
|
+
the plugins in your .rigor.yml, use `rigor plugins` (plural).
|
|
54
|
+
|
|
55
|
+
Subcommands:
|
|
56
|
+
list List bundled + example plugins (default)
|
|
57
|
+
path <name> Print the absolute directory path of <name>
|
|
58
|
+
print <name> Print <name>'s main lib source, with a header
|
|
59
|
+
root Print the gem root + key subdirectories
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
rigor plugin list
|
|
63
|
+
rigor plugin path rigor-activerecord
|
|
64
|
+
rigor plugin print rigor-activesupport-core-ext
|
|
65
|
+
rigor plugin root
|
|
66
|
+
USAGE
|
|
67
|
+
|
|
68
|
+
# The bundled plugins/examples/source live at `<gem_root>/...`.
|
|
69
|
+
# From `lib/rigor/cli/plugin_command.rb` the gem root is three
|
|
70
|
+
# directories up (matching SkillCommand::SKILLS_ROOT).
|
|
71
|
+
GEM_ROOT = File.expand_path("../../..", __dir__)
|
|
72
|
+
PLUGINS_ROOT = File.join(GEM_ROOT, "plugins")
|
|
73
|
+
EXAMPLES_ROOT = File.join(GEM_ROOT, "examples")
|
|
74
|
+
|
|
75
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
76
|
+
@argv = argv
|
|
77
|
+
@out = out
|
|
78
|
+
@err = err
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Integer] CLI exit status.
|
|
82
|
+
def run
|
|
83
|
+
subcommand = @argv.shift || "list"
|
|
84
|
+
|
|
85
|
+
case subcommand
|
|
86
|
+
when "list" then run_list
|
|
87
|
+
when "path" then run_path
|
|
88
|
+
when "print" then run_print
|
|
89
|
+
when "root" then run_root
|
|
90
|
+
when "-h", "--help", "help"
|
|
91
|
+
@out.puts(USAGE)
|
|
92
|
+
0
|
|
93
|
+
else
|
|
94
|
+
@err.puts("Unknown subcommand: #{subcommand}")
|
|
95
|
+
@err.puts(USAGE)
|
|
96
|
+
Rigor::CLI::EXIT_USAGE
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def run_list
|
|
103
|
+
plugins = discover(PLUGINS_ROOT)
|
|
104
|
+
examples = discover(EXAMPLES_ROOT)
|
|
105
|
+
if plugins.empty? && examples.empty?
|
|
106
|
+
@err.puts("No bundled plugins found under #{PLUGINS_ROOT}")
|
|
107
|
+
return 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
width = (plugins + examples).map { |p| p.fetch(:name).length }.max
|
|
111
|
+
emit_group("Production plugins", PLUGINS_ROOT, plugins, width)
|
|
112
|
+
emit_group("Example plugins (tutorials)", EXAMPLES_ROOT, examples, width)
|
|
113
|
+
@out.puts
|
|
114
|
+
@out.puts("Engine source root: #{GEM_ROOT}")
|
|
115
|
+
@out.puts(" public plugin API: #{File.join(GEM_ROOT, 'lib/rigor/plugin.rb')}")
|
|
116
|
+
@out.puts(docker_note)
|
|
117
|
+
0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def emit_group(label, root, entries, width)
|
|
121
|
+
return if entries.empty?
|
|
122
|
+
|
|
123
|
+
@out.puts("#{label} — under #{root}:")
|
|
124
|
+
entries.each do |entry|
|
|
125
|
+
@out.puts(format(" %-#{width}s %s", entry.fetch(:name), entry.fetch(:path)))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run_path
|
|
130
|
+
name = @argv.shift
|
|
131
|
+
return usage_error("`path` requires a plugin name") if name.nil?
|
|
132
|
+
|
|
133
|
+
plugin = find(name)
|
|
134
|
+
return name_error(name) if plugin.nil?
|
|
135
|
+
|
|
136
|
+
@out.puts(plugin.fetch(:path))
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def run_print
|
|
141
|
+
name = @argv.shift
|
|
142
|
+
return usage_error("`print` requires a plugin name") if name.nil?
|
|
143
|
+
|
|
144
|
+
plugin = find(name)
|
|
145
|
+
return name_error(name) if plugin.nil?
|
|
146
|
+
|
|
147
|
+
@out.puts(render_print_header(plugin))
|
|
148
|
+
@out.puts
|
|
149
|
+
entry = main_source_file(plugin)
|
|
150
|
+
if entry
|
|
151
|
+
@out.write(File.read(entry))
|
|
152
|
+
else
|
|
153
|
+
@out.puts("# (no lib/#{plugin.fetch(:name)}.rb entry file found; browse the directory above)")
|
|
154
|
+
end
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def run_root
|
|
159
|
+
@out.puts("rigortype gem root: #{GEM_ROOT}")
|
|
160
|
+
{
|
|
161
|
+
"lib (engine source)" => File.join(GEM_ROOT, "lib"),
|
|
162
|
+
"lib/rigor/plugin.rb (public plugin API)" => File.join(GEM_ROOT, "lib/rigor/plugin.rb"),
|
|
163
|
+
"plugins (production plugins)" => PLUGINS_ROOT,
|
|
164
|
+
"examples (tutorial plugins)" => EXAMPLES_ROOT,
|
|
165
|
+
"skills (Agent Skills)" => File.join(GEM_ROOT, "skills"),
|
|
166
|
+
"sig (bundled RBS)" => File.join(GEM_ROOT, "sig")
|
|
167
|
+
}.each do |label, path|
|
|
168
|
+
marker = File.exist?(path) ? "" : " (missing)"
|
|
169
|
+
@out.puts(format(" %<label>-42s %<path>s%<marker>s", label: label, path: path, marker: marker))
|
|
170
|
+
end
|
|
171
|
+
@out.puts(docker_note)
|
|
172
|
+
0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The header that precedes the plugin body when an author runs
|
|
176
|
+
# `rigor plugin print <name>`. `# `-prefixed so the combined
|
|
177
|
+
# output stays readable; the Ruby body below it is unchanged.
|
|
178
|
+
def render_print_header(plugin)
|
|
179
|
+
dir = plugin.fetch(:path)
|
|
180
|
+
sig = File.join(dir, "sig")
|
|
181
|
+
readme = File.join(dir, "README.md")
|
|
182
|
+
<<~HEADER.chomp
|
|
183
|
+
# Rigor plugin: #{plugin.fetch(:name)} (#{plugin.fetch(:kind)})
|
|
184
|
+
# Directory: #{dir}
|
|
185
|
+
# Lib: #{File.join(dir, 'lib')}
|
|
186
|
+
# Sig: #{File.directory?(sig) ? sig : '(none)'}
|
|
187
|
+
# README: #{File.file?(readme) ? readme : '(none)'}
|
|
188
|
+
#
|
|
189
|
+
# A real, working plugin shipped with rigortype #{Rigor::VERSION}.
|
|
190
|
+
# The main source body is below; read the other files from the
|
|
191
|
+
# paths above. To suppress `call.undefined-method` for methods a
|
|
192
|
+
# DSL generates, study how an RBS-bundle plugin ships `sig/` (see
|
|
193
|
+
# `rigor plugin print rigor-activesupport-core-ext`).
|
|
194
|
+
HEADER
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def discover(root)
|
|
198
|
+
return [] unless File.directory?(root)
|
|
199
|
+
|
|
200
|
+
kind = root == EXAMPLES_ROOT ? "example" : "production"
|
|
201
|
+
Dir.children(root).sort.filter_map do |name|
|
|
202
|
+
dir = File.join(root, name)
|
|
203
|
+
next unless File.directory?(dir)
|
|
204
|
+
|
|
205
|
+
{ name: name, path: dir, kind: kind }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Match the directory name with or without the conventional
|
|
210
|
+
# `rigor-` prefix, so both `rigor-activerecord` and
|
|
211
|
+
# `activerecord` resolve.
|
|
212
|
+
def find(name)
|
|
213
|
+
all = discover(PLUGINS_ROOT) + discover(EXAMPLES_ROOT)
|
|
214
|
+
all.find { |p| p.fetch(:name) == name } ||
|
|
215
|
+
all.find { |p| p.fetch(:name).delete_prefix("rigor-") == name.delete_prefix("rigor-") }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def main_source_file(plugin)
|
|
219
|
+
dir = plugin.fetch(:path)
|
|
220
|
+
candidate = File.join(dir, "lib", "#{plugin.fetch(:name)}.rb")
|
|
221
|
+
return candidate if File.file?(candidate)
|
|
222
|
+
|
|
223
|
+
Dir.glob(File.join(dir, "lib", "*.rb")).min
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def docker_note
|
|
227
|
+
"\nNote: paths are local to where `rigor` runs. If you run rigor in a " \
|
|
228
|
+
"container,\nread these files from the same filesystem (use " \
|
|
229
|
+
"`rigor plugin print` to inline a body)."
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def name_error(name)
|
|
233
|
+
@err.puts("Unknown plugin: #{name}")
|
|
234
|
+
@err.puts("Run `rigor plugin list` to see the bundled plugins.")
|
|
235
|
+
1
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def usage_error(message)
|
|
239
|
+
@err.puts(message)
|
|
240
|
+
@err.puts(USAGE)
|
|
241
|
+
Rigor::CLI::EXIT_USAGE
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -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
|