rigortype 0.1.11 → 0.1.13
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/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +37 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +195 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
- data/sig/rigor/scope.rbs +23 -0
- data/skills/rigor-baseline-reduce/SKILL.md +100 -0
- data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
- data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
- data/skills/rigor-plugin-author/SKILL.md +95 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
- data/skills/rigor-project-init/SKILL.md +129 -0
- data/skills/rigor-project-init/references/01-detect.md +101 -0
- data/skills/rigor-project-init/references/02-configure.md +185 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f7d66cf2c7a6c883fbbab59a805b1d1bca62867022e42d9b68d0b788525e831
|
|
4
|
+
data.tar.gz: c327399cf8c239da46f383de404cd15843d8ce220b5538f1905d72f4e082a59f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a69935a6c878eba22f3050041d044855b9f98eefa8dc3ab55bd33ca2ecce12efc5abffb2dd0fbc57edfd7736fe5ee4eb985694e4fbcddbc63f2e231bf48483a4
|
|
7
|
+
data.tar.gz: 58839858c8a5adcf8db74ef9d8c5ed2999c4459854a85c330b107ce3a933d08cb024cacc6c1303c638ab12ae1f5bc6559c14922ac13d17f41a5609631220fc28
|
|
@@ -57,6 +57,7 @@ module Rigor
|
|
|
57
57
|
# system; new rules MUST register here so user configuration
|
|
58
58
|
# can refer to them.
|
|
59
59
|
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
60
|
+
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
60
61
|
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
61
62
|
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
62
63
|
RULE_NIL_RECEIVER = "call.possible-nil-receiver"
|
|
@@ -72,6 +73,7 @@ module Rigor
|
|
|
72
73
|
|
|
73
74
|
ALL_RULES = [
|
|
74
75
|
RULE_UNDEFINED_METHOD,
|
|
76
|
+
RULE_UNRESOLVED_TOPLEVEL,
|
|
75
77
|
RULE_WRONG_ARITY,
|
|
76
78
|
RULE_ARGUMENT_TYPE,
|
|
77
79
|
RULE_NIL_RECEIVER,
|
|
@@ -162,6 +164,7 @@ module Rigor
|
|
|
162
164
|
def call_node_diagnostics(path, node, scope_index)
|
|
163
165
|
[
|
|
164
166
|
undefined_method_diagnostic(path, node, scope_index),
|
|
167
|
+
unresolved_toplevel_diagnostic(path, node, scope_index),
|
|
165
168
|
wrong_arity_diagnostic(path, node, scope_index),
|
|
166
169
|
argument_type_diagnostic(path, node, scope_index),
|
|
167
170
|
nil_receiver_diagnostic(path, node, scope_index),
|
|
@@ -365,10 +368,14 @@ module Rigor
|
|
|
365
368
|
return nil if open_receiver?(class_name, scope)
|
|
366
369
|
|
|
367
370
|
# Slice 7 phase 12 — suppress when the user has
|
|
368
|
-
# declared the method in source (
|
|
369
|
-
# `
|
|
371
|
+
# declared the method in source (`def` /
|
|
372
|
+
# `define_method`) OR in a `pre_eval:` monkey-patch
|
|
373
|
+
# file (ADR-17). Both paths are project-side method
|
|
374
|
+
# contributions the dispatcher already resolved; the
|
|
375
|
+
# rule must not surface a false `undefined-method`
|
|
376
|
+
# for them.
|
|
370
377
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
371
|
-
return nil if
|
|
378
|
+
return nil if source_declared_method?(scope, class_name, call_node.name, kind)
|
|
372
379
|
|
|
373
380
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
374
381
|
|
|
@@ -404,6 +411,92 @@ module Rigor
|
|
|
404
411
|
scope.environment.rbs_module?(receiver_type.class_name)
|
|
405
412
|
end
|
|
406
413
|
|
|
414
|
+
# Combined suppression probe for `undefined-method` /
|
|
415
|
+
# `unresolved-toplevel`. Returns true when the method is
|
|
416
|
+
# declared by any project-side contributor the dispatcher
|
|
417
|
+
# already resolves: an in-source `def` / `define_method`
|
|
418
|
+
# (`scope.discovered_method?`) OR an ADR-17 `pre_eval:`
|
|
419
|
+
# monkey-patch (`Environment#project_patched_methods`).
|
|
420
|
+
# Both paths sit at the same dispatcher precedence; the
|
|
421
|
+
# check must hold them together so neither rule fires a
|
|
422
|
+
# false positive.
|
|
423
|
+
def source_declared_method?(scope, class_name, method_name, kind)
|
|
424
|
+
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
425
|
+
|
|
426
|
+
project_patched_method?(scope, class_name, method_name, kind)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# ADR-17 § "Inference contract" — consults
|
|
430
|
+
# `Environment#project_patched_methods` so a `def` declared
|
|
431
|
+
# in a `pre_eval:` file suppresses the diagnostic at the
|
|
432
|
+
# same dispatcher precedence the registry holds for type
|
|
433
|
+
# inference (between plugins and dependency-source).
|
|
434
|
+
# Returns false when the environment carries no registry
|
|
435
|
+
# (legacy path) or the lookup misses.
|
|
436
|
+
def project_patched_method?(scope, class_name, method_name, kind)
|
|
437
|
+
environment = scope.environment
|
|
438
|
+
registry = environment&.project_patched_methods
|
|
439
|
+
return false if registry.nil? || registry.empty?
|
|
440
|
+
|
|
441
|
+
!registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind).nil?
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# ADR-34 — `call.unresolved-toplevel`. Fires on an
|
|
445
|
+
# implicit-self call (no explicit receiver) at toplevel
|
|
446
|
+
# scope (`scope.toplevel?`, i.e. outside any class /
|
|
447
|
+
# module body) whose name does not resolve against:
|
|
448
|
+
#
|
|
449
|
+
# 1. A same-file toplevel `def` via
|
|
450
|
+
# {Scope#top_level_def_for}.
|
|
451
|
+
# 2. The ADR-17 `ProjectPatchedMethods` registry under
|
|
452
|
+
# `(Object, name, :instance)` — projects declare
|
|
453
|
+
# their toplevel-injecting monkey-patches in
|
|
454
|
+
# `.rigor.yml`'s `pre_eval:` array as the canonical
|
|
455
|
+
# opt-out per ADR-34 WD2.
|
|
456
|
+
# 3. The standard `Kernel` / `Object` private-method
|
|
457
|
+
# surface (`puts`, `p`, `require`, `loop`, `raise`,
|
|
458
|
+
# …) drawn from the loaded RBS environment.
|
|
459
|
+
#
|
|
460
|
+
# The rule deliberately does NOT generalise to
|
|
461
|
+
# implicit-self calls inside `def` / `class` / `module`
|
|
462
|
+
# bodies — ADR-24 WD3's lenient-on-unresolved default
|
|
463
|
+
# stays in force there. ADR-24 WD4's gated class-body
|
|
464
|
+
# diagnostic is a separate decision this ADR does not
|
|
465
|
+
# open.
|
|
466
|
+
#
|
|
467
|
+
# Authored severity is `:warning`; the severity profile
|
|
468
|
+
# remaps it (`strict` → `:error`, `balanced` →
|
|
469
|
+
# `:warning`, `lenient` → `:off` / suppressed).
|
|
470
|
+
def unresolved_toplevel_diagnostic(path, call_node, scope_index)
|
|
471
|
+
return nil unless call_node.receiver.nil?
|
|
472
|
+
|
|
473
|
+
scope = scope_index[call_node]
|
|
474
|
+
return nil if scope.nil?
|
|
475
|
+
return nil unless scope.toplevel?
|
|
476
|
+
|
|
477
|
+
name = call_node.name
|
|
478
|
+
return nil if scope.top_level_def_for(name)
|
|
479
|
+
return nil if source_declared_method?(scope, "Object", name, :instance)
|
|
480
|
+
return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
|
|
481
|
+
|
|
482
|
+
build_unresolved_toplevel_diagnostic(path, call_node)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def build_unresolved_toplevel_diagnostic(path, call_node)
|
|
486
|
+
location = call_node.message_loc || call_node.location
|
|
487
|
+
Diagnostic.new(
|
|
488
|
+
path: path,
|
|
489
|
+
line: location.start_line,
|
|
490
|
+
column: location.start_column + 1,
|
|
491
|
+
message: "unresolved toplevel call to `#{call_node.name}`. " \
|
|
492
|
+
"If a project file defines `#{call_node.name}` via a toplevel " \
|
|
493
|
+
"`def` or a monkey-patch on Object/Kernel, list that file in " \
|
|
494
|
+
"`.rigor.yml`'s `pre_eval:` (ADR-17) so the analyzer sees it.",
|
|
495
|
+
severity: :warning,
|
|
496
|
+
rule: RULE_UNRESOLVED_TOPLEVEL
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
407
500
|
# Returns a qualified class name for the in-scope check.
|
|
408
501
|
# Nominal / Singleton carry a single-class identity
|
|
409
502
|
# directly. Constant projects to its value's class.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Detects when a `.rb` file is actually an ERB template — a Rails
|
|
6
|
+
# generator `templates/foo.rb` shape that uses `<%= ... %>` to
|
|
7
|
+
# interpolate identifiers. Prism rejects the template as invalid
|
|
8
|
+
# Ruby and the analyzer emits one parse-error diagnostic per scrap
|
|
9
|
+
# the parser tripped over (≈ 20 on Redmine's
|
|
10
|
+
# `lib/generators/redmine_plugin_model/templates/migration.rb`),
|
|
11
|
+
# all of them noise from the user's perspective. The runner / worker
|
|
12
|
+
# consults this detector before turning parse errors into
|
|
13
|
+
# diagnostics; an ERB-shaped source is silently skipped.
|
|
14
|
+
#
|
|
15
|
+
# Detection is byte-level on the raw source: any occurrence of the
|
|
16
|
+
# `%>` closing marker proves the file is an ERB template. `%>`
|
|
17
|
+
# cannot appear in valid Ruby — `%` is a binary operator that
|
|
18
|
+
# requires an operand on its right side, never `>`. The opening
|
|
19
|
+
# `<%` is ambiguous in principle (`x<%y` parses as `x < %y`, a
|
|
20
|
+
# comparison against a `%`-literal) but a real Ruby file with that
|
|
21
|
+
# shape would still not contain the closing `%>`.
|
|
22
|
+
module ErbTemplateDetector
|
|
23
|
+
ERB_CLOSING_MARKER = /%>/
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# @param parse_result [Prism::ParseResult]
|
|
28
|
+
# @return [Boolean] true when the parsed source looks like an
|
|
29
|
+
# ERB template (parse errors expected; analysis should skip).
|
|
30
|
+
def template?(parse_result)
|
|
31
|
+
source = parse_result.source.source
|
|
32
|
+
return false unless source.is_a?(String) && !source.empty?
|
|
33
|
+
|
|
34
|
+
ERB_CLOSING_MARKER.match?(source)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -20,6 +20,7 @@ require_relative "buffer_binding"
|
|
|
20
20
|
require_relative "check_rules"
|
|
21
21
|
require_relative "dependency_source_inference"
|
|
22
22
|
require_relative "diagnostic"
|
|
23
|
+
require_relative "erb_template_detector"
|
|
23
24
|
require_relative "project_scan"
|
|
24
25
|
require_relative "result"
|
|
25
26
|
require_relative "run_stats"
|
|
@@ -1457,7 +1458,11 @@ module Rigor
|
|
|
1457
1458
|
|
|
1458
1459
|
def analyze_file(path, environment) # rubocop:disable Metrics/MethodLength
|
|
1459
1460
|
parse_result = parse_source(path)
|
|
1460
|
-
|
|
1461
|
+
unless parse_result.errors.empty?
|
|
1462
|
+
return [] if ErbTemplateDetector.template?(parse_result)
|
|
1463
|
+
|
|
1464
|
+
return parse_diagnostics(path, parse_result)
|
|
1465
|
+
end
|
|
1461
1466
|
|
|
1462
1467
|
scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
1463
1468
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
@@ -15,6 +15,7 @@ require_relative "../inference/method_dispatcher/file_folding"
|
|
|
15
15
|
require_relative "check_rules"
|
|
16
16
|
require_relative "dependency_source_inference"
|
|
17
17
|
require_relative "diagnostic"
|
|
18
|
+
require_relative "erb_template_detector"
|
|
18
19
|
|
|
19
20
|
module Rigor
|
|
20
21
|
module Analysis
|
|
@@ -158,7 +159,11 @@ module Rigor
|
|
|
158
159
|
# is a per-run aggregate concern handled by the caller.
|
|
159
160
|
def analyze(path)
|
|
160
161
|
parse_result = parse_source(path)
|
|
161
|
-
|
|
162
|
+
unless parse_result.errors.empty?
|
|
163
|
+
return [] if ErbTemplateDetector.template?(parse_result)
|
|
164
|
+
|
|
165
|
+
return parse_diagnostics(path, parse_result)
|
|
166
|
+
end
|
|
162
167
|
|
|
163
168
|
scope = Scope.empty(environment: @environment, source_path: path)
|
|
164
169
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "../configuration"
|
|
6
|
+
require_relative "../plugin"
|
|
7
|
+
require_relative "../plugin/loader"
|
|
8
|
+
require_relative "../plugin/services"
|
|
9
|
+
require_relative "../reflection"
|
|
10
|
+
require_relative "../type/combinator"
|
|
11
|
+
require_relative "plugins_renderer"
|
|
12
|
+
|
|
13
|
+
module Rigor
|
|
14
|
+
class CLI
|
|
15
|
+
# `rigor plugins` — reports the activation status of every
|
|
16
|
+
# plugin entry in `.rigor.yml` so users (and the
|
|
17
|
+
# `rigor-project-init` SKILL, CI gates, `rigor init`) can
|
|
18
|
+
# verify their plugin configuration is actually doing what
|
|
19
|
+
# they think.
|
|
20
|
+
#
|
|
21
|
+
# The command is read-only and idempotent: it loads the
|
|
22
|
+
# project's `.rigor.yml` (same discovery as `rigor check`),
|
|
23
|
+
# runs `Plugin::Loader.load` to attempt instantiation, then
|
|
24
|
+
# prints a table of:
|
|
25
|
+
#
|
|
26
|
+
# - load status (`loaded` / `load-error` with reason);
|
|
27
|
+
# - resolved manifest id, version, description;
|
|
28
|
+
# - `signature_paths:` (absolute paths + per-dir `.rbs` count);
|
|
29
|
+
# - every manifest-declared extension surface
|
|
30
|
+
# (`open_receivers:` / `owns_receivers:` / `produces:` /
|
|
31
|
+
# `consumes:` / `block_as_methods:` / `heredoc_templates:` /
|
|
32
|
+
# `trait_registries:` / `external_files:` /
|
|
33
|
+
# `type_node_resolvers:` / `hkt_registrations:` /
|
|
34
|
+
# `hkt_definitions:` / `protocol_contracts:` /
|
|
35
|
+
# `source_rbs_synthesizer:`).
|
|
36
|
+
#
|
|
37
|
+
# Output formats: `text` (default, human-readable table) and
|
|
38
|
+
# `json` (for tooling — SKILLs, CI gates, editor integrations).
|
|
39
|
+
#
|
|
40
|
+
# Exit codes:
|
|
41
|
+
# - `0` — every configured plugin loaded.
|
|
42
|
+
# - `1` — at least one plugin failed to load AND `--strict`
|
|
43
|
+
# was passed. Without `--strict` the command always exits 0;
|
|
44
|
+
# load errors are reported but not treated as a gate failure
|
|
45
|
+
# (matching `rigor triage`'s advisory shape).
|
|
46
|
+
#
|
|
47
|
+
# Future expansion (not in this slice):
|
|
48
|
+
# - Per-plugin diagnostic counts (would require running the
|
|
49
|
+
# full analysis pipeline; out of scope for an inspection
|
|
50
|
+
# command).
|
|
51
|
+
# - Verification that `signature_paths` actually merged into
|
|
52
|
+
# the RBS environment without conflict (requires constructing
|
|
53
|
+
# the Environment, which is heavier than the loader-only
|
|
54
|
+
# pass this slice does).
|
|
55
|
+
class PluginsCommand
|
|
56
|
+
USAGE = "Usage: rigor plugins [options]"
|
|
57
|
+
|
|
58
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
59
|
+
@argv = argv
|
|
60
|
+
@out = out
|
|
61
|
+
@err = err
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Integer] CLI exit status.
|
|
65
|
+
def run
|
|
66
|
+
options = parse_options
|
|
67
|
+
config_path = options.fetch(:config) || Configuration.discover
|
|
68
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
69
|
+
rows = build_rows(configuration)
|
|
70
|
+
|
|
71
|
+
renderer = PluginsRenderer.new(rows: rows, configuration_path: config_path)
|
|
72
|
+
@out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
|
|
73
|
+
|
|
74
|
+
any_load_errors = rows.any? { |row| row.fetch(:status) == :load_error }
|
|
75
|
+
return 1 if any_load_errors && options.fetch(:strict)
|
|
76
|
+
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def parse_options
|
|
83
|
+
options = { config: nil, format: "text", strict: false }
|
|
84
|
+
OptionParser.new do |opts|
|
|
85
|
+
opts.banner = USAGE
|
|
86
|
+
opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
|
|
87
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
88
|
+
opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
|
|
89
|
+
end.parse!(@argv)
|
|
90
|
+
validate!(options)
|
|
91
|
+
options
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate!(options)
|
|
95
|
+
return if %w[text json].include?(options.fetch(:format))
|
|
96
|
+
|
|
97
|
+
raise OptionParser::InvalidArgument, "unsupported format: #{options.fetch(:format)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Runs the plugin loader against the project configuration
|
|
101
|
+
# and returns a row per declared plugin entry. Each row is
|
|
102
|
+
# a plain Hash with the fields enumerated in the class
|
|
103
|
+
# docstring so the renderer (text + JSON) reads from a
|
|
104
|
+
# single shape.
|
|
105
|
+
#
|
|
106
|
+
# The loader catches require / init failures and surfaces
|
|
107
|
+
# them through `registry.load_errors`; we merge those back
|
|
108
|
+
# into the per-entry rows by matching on `plugin_ref`.
|
|
109
|
+
def build_rows(configuration)
|
|
110
|
+
services = build_services(configuration)
|
|
111
|
+
registry = Plugin::Loader.load(configuration: configuration, services: services)
|
|
112
|
+
|
|
113
|
+
rows = configuration.plugins.map { |entry| row_for_entry(entry, registry) }
|
|
114
|
+
# Surface registry-level errors that did not bind to an
|
|
115
|
+
# entry (e.g. dependency-cycle errors that name multiple
|
|
116
|
+
# plugins). The renderer treats these as "orphan" errors.
|
|
117
|
+
orphan_errors = orphan_load_errors(registry, rows)
|
|
118
|
+
rows + orphan_errors
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_services(configuration)
|
|
122
|
+
Plugin::Services.new(
|
|
123
|
+
reflection: Reflection,
|
|
124
|
+
type: Type::Combinator,
|
|
125
|
+
configuration: configuration,
|
|
126
|
+
cache_store: nil
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def row_for_entry(entry, registry)
|
|
131
|
+
gem_name = entry_gem_name(entry)
|
|
132
|
+
config = entry_config(entry)
|
|
133
|
+
plugin = registry.plugins.find do |p|
|
|
134
|
+
# The loader has already matched the entry to a plugin
|
|
135
|
+
# class; we can identify it by either the gem name (when
|
|
136
|
+
# the entry was a String) or the explicit id (when the
|
|
137
|
+
# entry was a Hash with `id:`).
|
|
138
|
+
plugin_matches_entry?(p, gem_name, entry_id(entry))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if plugin
|
|
142
|
+
loaded_row(plugin, gem_name, config)
|
|
143
|
+
else
|
|
144
|
+
# Find the load error whose plugin_ref names this entry
|
|
145
|
+
# (the ref is set by Loader to the gem name on require
|
|
146
|
+
# failures and to the manifest id on later failures).
|
|
147
|
+
error = registry.load_errors.find do |e|
|
|
148
|
+
ref = e.plugin_ref.to_s
|
|
149
|
+
ref == gem_name || ref == entry_id(entry).to_s
|
|
150
|
+
end
|
|
151
|
+
load_error_row(gem_name, entry_id(entry), config, error)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def plugin_matches_entry?(plugin, gem_name, entry_id)
|
|
156
|
+
return true if entry_id && plugin.manifest.id == entry_id
|
|
157
|
+
|
|
158
|
+
# A bare gem entry conventionally has manifest.id equal to
|
|
159
|
+
# the gem name with the `rigor-` prefix stripped.
|
|
160
|
+
derived_id = gem_name.delete_prefix("rigor-")
|
|
161
|
+
[derived_id, gem_name].include?(plugin.manifest.id)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def loaded_row(plugin, gem_name, config)
|
|
165
|
+
manifest = plugin.manifest
|
|
166
|
+
identity_fields(gem_name, manifest, config)
|
|
167
|
+
.merge(extension_fields(plugin, manifest))
|
|
168
|
+
.merge(load_error: nil)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def identity_fields(gem_name, manifest, config)
|
|
172
|
+
{
|
|
173
|
+
gem: gem_name,
|
|
174
|
+
status: :loaded,
|
|
175
|
+
id: manifest.id,
|
|
176
|
+
version: manifest.version,
|
|
177
|
+
description: manifest.description,
|
|
178
|
+
config: config
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def extension_fields(plugin, manifest)
|
|
183
|
+
{
|
|
184
|
+
signature_paths: signature_path_rows(plugin),
|
|
185
|
+
open_receivers: manifest.open_receivers.dup,
|
|
186
|
+
owns_receivers: manifest.owns_receivers.dup,
|
|
187
|
+
produces: manifest.produces.map(&:to_s),
|
|
188
|
+
consumes: manifest.consumes.map { |c| "#{c.plugin_id}/#{c.name}#{'?' if c.optional}" },
|
|
189
|
+
source_rbs_synthesizer: !manifest.source_rbs_synthesizer.nil?
|
|
190
|
+
}.merge(extension_count_fields(manifest))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def extension_count_fields(manifest)
|
|
194
|
+
{
|
|
195
|
+
block_as_methods: manifest.block_as_methods.size,
|
|
196
|
+
heredoc_templates: manifest.heredoc_templates.size,
|
|
197
|
+
trait_registries: manifest.trait_registries.size,
|
|
198
|
+
external_files: manifest.external_files.size,
|
|
199
|
+
type_node_resolvers: manifest.type_node_resolvers.size,
|
|
200
|
+
hkt_registrations: manifest.hkt_registrations.size,
|
|
201
|
+
hkt_definitions: manifest.hkt_definitions.size,
|
|
202
|
+
protocol_contracts: manifest.protocol_contracts.size
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def load_error_row(gem_name, entry_id, config, error)
|
|
207
|
+
{
|
|
208
|
+
gem: gem_name,
|
|
209
|
+
status: :load_error,
|
|
210
|
+
id: entry_id,
|
|
211
|
+
version: nil,
|
|
212
|
+
description: nil,
|
|
213
|
+
config: config,
|
|
214
|
+
signature_paths: [],
|
|
215
|
+
open_receivers: [],
|
|
216
|
+
owns_receivers: [],
|
|
217
|
+
produces: [],
|
|
218
|
+
consumes: [],
|
|
219
|
+
block_as_methods: 0,
|
|
220
|
+
heredoc_templates: 0,
|
|
221
|
+
trait_registries: 0,
|
|
222
|
+
external_files: 0,
|
|
223
|
+
type_node_resolvers: 0,
|
|
224
|
+
hkt_registrations: 0,
|
|
225
|
+
hkt_definitions: 0,
|
|
226
|
+
protocol_contracts: 0,
|
|
227
|
+
source_rbs_synthesizer: false,
|
|
228
|
+
load_error: error&.message || "plugin did not register or could not be matched to a registered class"
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# For each `signature_paths:` directory the plugin resolves
|
|
233
|
+
# against its gem root, report the absolute path and a
|
|
234
|
+
# quick `.rbs`-file count. The count is a sanity signal:
|
|
235
|
+
# an empty bundle directory loads silently today but
|
|
236
|
+
# contributes nothing.
|
|
237
|
+
def signature_path_rows(plugin)
|
|
238
|
+
plugin.signature_paths.map do |abs|
|
|
239
|
+
{
|
|
240
|
+
path: abs,
|
|
241
|
+
exists: File.directory?(abs),
|
|
242
|
+
rbs_files: rbs_file_count(abs)
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def rbs_file_count(dir)
|
|
248
|
+
return 0 unless File.directory?(dir)
|
|
249
|
+
|
|
250
|
+
Dir.glob(File.join(dir, "**", "*.rbs")).size
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def entry_gem_name(entry)
|
|
254
|
+
case entry
|
|
255
|
+
when String then entry
|
|
256
|
+
when Hash
|
|
257
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
258
|
+
(string_keyed["gem"] || string_keyed["id"]).to_s
|
|
259
|
+
else entry.to_s
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def entry_id(entry)
|
|
264
|
+
case entry
|
|
265
|
+
when Hash
|
|
266
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
267
|
+
string_keyed["id"]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def entry_config(entry)
|
|
272
|
+
case entry
|
|
273
|
+
when Hash
|
|
274
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
275
|
+
string_keyed["config"] || {}
|
|
276
|
+
else {}
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Build orphan-error rows for load errors whose `plugin_ref`
|
|
281
|
+
# did not bind to any configured plugin entry (e.g. a
|
|
282
|
+
# dependency-cycle naming a plugin we already accounted for
|
|
283
|
+
# but reported alongside another). De-duplicates against
|
|
284
|
+
# errors we already attached.
|
|
285
|
+
def orphan_load_errors(registry, rows)
|
|
286
|
+
attached_refs = rows.compact.flat_map { |row| [row[:gem], row[:id]].compact }.to_set
|
|
287
|
+
unattached = registry.load_errors.reject { |error| attached_refs.include?(error.plugin_ref.to_s) }
|
|
288
|
+
unattached.map do |error|
|
|
289
|
+
{
|
|
290
|
+
gem: error.plugin_ref.to_s,
|
|
291
|
+
status: :load_error,
|
|
292
|
+
id: nil,
|
|
293
|
+
version: nil,
|
|
294
|
+
description: nil,
|
|
295
|
+
config: {},
|
|
296
|
+
signature_paths: [],
|
|
297
|
+
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
298
|
+
block_as_methods: 0, heredoc_templates: 0, trait_registries: 0,
|
|
299
|
+
external_files: 0, type_node_resolvers: 0,
|
|
300
|
+
hkt_registrations: 0, hkt_definitions: 0,
|
|
301
|
+
protocol_contracts: 0, source_rbs_synthesizer: false,
|
|
302
|
+
load_error: error.message
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|