rigortype 0.1.18 → 0.2.0
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- 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 +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# ADR-63 Tier 2 — aggregates per-file {Protection::MutationScanner}
|
|
6
|
+
# results into a project-level *effectiveness* report: the kill ratio (when
|
|
7
|
+
# a type-visible bug was introduced, how often Rigor caught it), the per-file
|
|
8
|
+
# breakdown, and a ranked "add a type here" list keyed by the method whose
|
|
9
|
+
# breakage Rigor most often *missed* — the sites where a receiver annotation
|
|
10
|
+
# would buy real catching power.
|
|
11
|
+
#
|
|
12
|
+
# The framing is load-bearing (ADR-63 Criterion A / ADR-62 Criterion A): the
|
|
13
|
+
# number is *effectiveness*, the survivors are *missed breakages / where to
|
|
14
|
+
# add a type*, never "your code is broken".
|
|
15
|
+
FileEffectiveness = Data.define(:path, :killed, :survived, :ratio)
|
|
16
|
+
MissedBreakage = Data.define(:method_name, :count, :examples)
|
|
17
|
+
|
|
18
|
+
MutationProtectionReport = Data.define(:files, :missed, :parse_errors) do
|
|
19
|
+
def total_killed = files.sum(&:killed)
|
|
20
|
+
def total_survived = files.sum(&:survived)
|
|
21
|
+
def grand_total = total_killed + total_survived
|
|
22
|
+
def ratio = grand_total.zero? ? 1.0 : total_killed.to_f / grand_total
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
"mode" => "mutation",
|
|
27
|
+
"killed" => total_killed,
|
|
28
|
+
"survived" => total_survived,
|
|
29
|
+
"effectiveness_ratio" => ratio.round(4),
|
|
30
|
+
"files" => files.map do |f|
|
|
31
|
+
{ "path" => f.path, "killed" => f.killed,
|
|
32
|
+
"survived" => f.survived, "ratio" => f.ratio.round(4) }
|
|
33
|
+
end,
|
|
34
|
+
"add_a_type_here" => missed.map do |m|
|
|
35
|
+
{ "method" => m.method_name, "count" => m.count, "examples" => m.examples }
|
|
36
|
+
end,
|
|
37
|
+
"parse_errors" => parse_errors
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class MutationProtectionAccumulator
|
|
43
|
+
def initialize
|
|
44
|
+
@files = []
|
|
45
|
+
@missed = Hash.new { |h, k| h[k] = { count: 0, examples: [] } }
|
|
46
|
+
@parse_errors = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def absorb(file_result)
|
|
50
|
+
@files << FileEffectiveness.new(
|
|
51
|
+
path: file_result.path, killed: file_result.killed,
|
|
52
|
+
survived: file_result.survived, ratio: file_result.ratio
|
|
53
|
+
)
|
|
54
|
+
file_result.sites.each do |site|
|
|
55
|
+
bucket = @missed[site.method_name]
|
|
56
|
+
bucket[:count] += 1
|
|
57
|
+
bucket[:examples] << "#{file_result.path}:#{site.line}" if bucket[:examples].size < 3
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def record_parse_error(path, errors)
|
|
62
|
+
@parse_errors << { "path" => path, "errors" => errors.size }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_report
|
|
66
|
+
missed = @missed
|
|
67
|
+
.map { |method, v| MissedBreakage.new(method_name: method, count: v[:count], examples: v[:examples]) }
|
|
68
|
+
.sort_by { |m| [-m.count, m.method_name] }
|
|
69
|
+
MutationProtectionReport.new(files: @files, missed: missed, parse_errors: @parse_errors)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/rigor/cli/options.rb
CHANGED
|
@@ -17,6 +17,15 @@ module Rigor
|
|
|
17
17
|
module Options
|
|
18
18
|
module_function
|
|
19
19
|
|
|
20
|
+
# Defines the standard `--config=PATH` flag on `parser`, writing the
|
|
21
|
+
# path into `options[:config]`. Used by every subcommand that loads a
|
|
22
|
+
# `.rigor.yml`; the few whose `--config` help text is intentionally
|
|
23
|
+
# bespoke (`diff`, `mcp`, `show-bleedingedge`) keep their own
|
|
24
|
+
# `parser.on` rather than this shared wording.
|
|
25
|
+
def add_config(parser, options)
|
|
26
|
+
parser.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
# Defines the `--tmp-file` / `--instead-of` editor-mode flag pair
|
|
21
30
|
# on `parser`, writing into `options`.
|
|
22
31
|
def add_editor_mode(parser, options)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "options"
|
|
6
7
|
require_relative "../plugin"
|
|
7
8
|
require_relative "../plugin/loader"
|
|
8
9
|
require_relative "../plugin/services"
|
|
@@ -30,7 +31,7 @@ module Rigor
|
|
|
30
31
|
# - every manifest-declared extension surface
|
|
31
32
|
# (`open_receivers:` / `owns_receivers:` / `produces:` /
|
|
32
33
|
# `consumes:` / `block_as_methods:` / `heredoc_templates:` /
|
|
33
|
-
# `trait_registries:` /
|
|
34
|
+
# `trait_registries:` /
|
|
34
35
|
# `type_node_resolvers:` / `hkt_registrations:` /
|
|
35
36
|
# `hkt_definitions:` / `protocol_contracts:` /
|
|
36
37
|
# `source_rbs_synthesizer:`);
|
|
@@ -102,7 +103,7 @@ module Rigor
|
|
|
102
103
|
options = { config: nil, format: "text", strict: false, capabilities: false }
|
|
103
104
|
OptionParser.new do |opts|
|
|
104
105
|
opts.banner = USAGE
|
|
105
|
-
|
|
106
|
+
Options.add_config(opts, options)
|
|
106
107
|
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
107
108
|
opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
|
|
108
109
|
opts.on("--capabilities", "Emit the per-plugin extension-protocol catalogue (ADR-37)") do
|
|
@@ -233,7 +234,6 @@ module Rigor
|
|
|
233
234
|
block_as_methods: manifest.block_as_methods.size,
|
|
234
235
|
heredoc_templates: manifest.heredoc_templates.size,
|
|
235
236
|
trait_registries: manifest.trait_registries.size,
|
|
236
|
-
external_files: manifest.external_files.size,
|
|
237
237
|
type_node_resolvers: manifest.type_node_resolvers.size,
|
|
238
238
|
hkt_registrations: manifest.hkt_registrations.size,
|
|
239
239
|
hkt_definitions: manifest.hkt_definitions.size,
|
|
@@ -257,7 +257,6 @@ module Rigor
|
|
|
257
257
|
block_as_methods: 0,
|
|
258
258
|
heredoc_templates: 0,
|
|
259
259
|
trait_registries: 0,
|
|
260
|
-
external_files: 0,
|
|
261
260
|
type_node_resolvers: 0,
|
|
262
261
|
hkt_registrations: 0,
|
|
263
262
|
hkt_definitions: 0,
|
|
@@ -337,7 +336,7 @@ module Rigor
|
|
|
337
336
|
signature_paths: [],
|
|
338
337
|
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
339
338
|
block_as_methods: 0, heredoc_templates: 0, trait_registries: 0,
|
|
340
|
-
|
|
339
|
+
type_node_resolvers: 0,
|
|
341
340
|
hkt_registrations: 0, hkt_definitions: 0,
|
|
342
341
|
protocol_contracts: 0, source_rbs_synthesizer: false,
|
|
343
342
|
node_rule_types: [], dynamic_return_receivers: [], type_specifier_methods: [],
|
|
@@ -209,7 +209,6 @@ module Rigor
|
|
|
209
209
|
parts << "block_as_methods=#{row[:block_as_methods]}" if row[:block_as_methods].positive?
|
|
210
210
|
parts << "heredoc_templates=#{row[:heredoc_templates]}" if row[:heredoc_templates].positive?
|
|
211
211
|
parts << "trait_registries=#{row[:trait_registries]}" if row[:trait_registries].positive?
|
|
212
|
-
parts << "external_files=#{row[:external_files]}" if row[:external_files].positive?
|
|
213
212
|
return [] if parts.empty?
|
|
214
213
|
|
|
215
214
|
[" macro substrate: #{parts.join(', ')}"]
|
|
@@ -233,7 +232,6 @@ module Rigor
|
|
|
233
232
|
"block_as_methods" => row[:block_as_methods],
|
|
234
233
|
"heredoc_templates" => row[:heredoc_templates],
|
|
235
234
|
"trait_registries" => row[:trait_registries],
|
|
236
|
-
"external_files" => row[:external_files],
|
|
237
235
|
"type_node_resolvers" => row[:type_node_resolvers],
|
|
238
236
|
"hkt_registrations" => row[:hkt_registrations],
|
|
239
237
|
"hkt_definitions" => row[:hkt_definitions],
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renders an {ProtectionReport} (ADR-63 Tier 1) as text or JSON. The text
|
|
8
|
+
# form leads with the protected ratio, then the highest-traffic untyped
|
|
9
|
+
# dispatches ("add a type here"), then the lowest-protected files. The
|
|
10
|
+
# framing is always *where to add a type*, never "your code is broken".
|
|
11
|
+
class ProtectionRenderer
|
|
12
|
+
TOP_CALLS = 15
|
|
13
|
+
TOP_FILES = 10
|
|
14
|
+
|
|
15
|
+
def initialize(out:)
|
|
16
|
+
@out = out
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render(report, format:)
|
|
20
|
+
format == "json" ? render_json(report) : render_text(report)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_json(report)
|
|
26
|
+
@out.puts(JSON.pretty_generate(report.to_h))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_text(report)
|
|
30
|
+
pct = (report.ratio * 100).round(1)
|
|
31
|
+
@out.puts "Type-protection coverage (Tier 1 — dispatch-site receiver concreteness)"
|
|
32
|
+
@out.puts " protected dispatch sites: #{report.total_protected} / #{report.grand_total} (#{pct}%)"
|
|
33
|
+
@out.puts " (protected = Rigor can catch a wrong call here; an upper bound on real protection)"
|
|
34
|
+
render_untyped_calls(report)
|
|
35
|
+
render_files(report)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def render_untyped_calls(report)
|
|
39
|
+
calls = report.untyped_calls
|
|
40
|
+
return if calls.empty?
|
|
41
|
+
|
|
42
|
+
@out.puts "\nAdd a type here — methods most often called on an untyped receiver:"
|
|
43
|
+
calls.first(TOP_CALLS).each do |call|
|
|
44
|
+
@out.puts format(" %<count>-5d #%<method>s e.g. %<sites>s",
|
|
45
|
+
count: call.count, method: call.method_name, sites: call.examples.join(" "))
|
|
46
|
+
end
|
|
47
|
+
@out.puts " (#{calls.size - TOP_CALLS} more)" if calls.size > TOP_CALLS
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_files(report)
|
|
51
|
+
worst = report.files.reject { |f| f.unprotected_count.zero? }.sort_by(&:ratio).first(TOP_FILES)
|
|
52
|
+
return if worst.empty?
|
|
53
|
+
|
|
54
|
+
@out.puts "\nLeast-protected files:"
|
|
55
|
+
worst.each do |file|
|
|
56
|
+
total = file.protected_count + file.unprotected_count
|
|
57
|
+
@out.puts format(" %<pct>5.1f%% %<path>s (%<n>d/%<total>d protected)",
|
|
58
|
+
pct: file.ratio * 100, path: file.path, n: file.protected_count, total: total)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# ADR-63 Tier 1 — aggregates per-file {Inference::ProtectionScanner}
|
|
6
|
+
# results into a project-level protection report: the protected ratio, the
|
|
7
|
+
# per-file breakdown, and a ranked "add a type here" list keyed by the
|
|
8
|
+
# method called on an unprotected (`Dynamic`) receiver — the highest-traffic
|
|
9
|
+
# untyped dispatches, where a receiver annotation buys the most catching
|
|
10
|
+
# power.
|
|
11
|
+
FileProtection = Data.define(:path, :protected_count, :unprotected_count, :ratio)
|
|
12
|
+
UntypedCall = Data.define(:method_name, :count, :examples)
|
|
13
|
+
|
|
14
|
+
ProtectionReport = Data.define(:files, :untyped_calls, :parse_errors) do
|
|
15
|
+
def total_protected = files.sum(&:protected_count)
|
|
16
|
+
def total_unprotected = files.sum(&:unprotected_count)
|
|
17
|
+
def grand_total = total_protected + total_unprotected
|
|
18
|
+
def ratio = grand_total.zero? ? 1.0 : total_protected.to_f / grand_total
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{
|
|
22
|
+
"protected" => total_protected,
|
|
23
|
+
"unprotected" => total_unprotected,
|
|
24
|
+
"protection_ratio" => ratio.round(4),
|
|
25
|
+
"files" => files.map do |f|
|
|
26
|
+
{ "path" => f.path, "protected" => f.protected_count,
|
|
27
|
+
"unprotected" => f.unprotected_count, "ratio" => f.ratio.round(4) }
|
|
28
|
+
end,
|
|
29
|
+
"add_a_type_here" => untyped_calls.map do |c|
|
|
30
|
+
{ "method" => c.method_name, "count" => c.count, "examples" => c.examples }
|
|
31
|
+
end,
|
|
32
|
+
"parse_errors" => parse_errors
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class ProtectionAccumulator
|
|
38
|
+
def initialize
|
|
39
|
+
@files = []
|
|
40
|
+
@calls = Hash.new { |h, k| h[k] = { count: 0, examples: [] } }
|
|
41
|
+
@parse_errors = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def absorb(path, file_result)
|
|
45
|
+
@files << FileProtection.new(
|
|
46
|
+
path: path, protected_count: file_result.protected_count,
|
|
47
|
+
unprotected_count: file_result.unprotected_count, ratio: file_result.ratio
|
|
48
|
+
)
|
|
49
|
+
file_result.sites.each do |site|
|
|
50
|
+
bucket = @calls[site.method_name]
|
|
51
|
+
bucket[:count] += 1
|
|
52
|
+
bucket[:examples] << "#{path}:#{site.line}" if bucket[:examples].size < 3
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_parse_error(path, errors)
|
|
57
|
+
@parse_errors << { "path" => path, "errors" => errors.size }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_report
|
|
61
|
+
untyped = @calls
|
|
62
|
+
.map { |method, v| UntypedCall.new(method_name: method, count: v[:count], examples: v[:examples]) }
|
|
63
|
+
.sort_by { |c| [-c.count, c.method_name] }
|
|
64
|
+
ProtectionReport.new(files: @files, untyped_calls: untyped, parse_errors: @parse_errors)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optionparser"
|
|
5
|
+
|
|
6
|
+
require_relative "../bleeding_edge"
|
|
7
|
+
require_relative "../configuration"
|
|
8
|
+
require_relative "command"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
class CLI
|
|
12
|
+
# Executes `rigor show-bleedingedge` (ADR-50 § WD2).
|
|
13
|
+
#
|
|
14
|
+
# Prints the bleeding-edge overlay — the Rigor-maintained set of the
|
|
15
|
+
# next major's queued changes ({Rigor::BleedingEdge}) — as an
|
|
16
|
+
# explicit list, and reports which of them the project's
|
|
17
|
+
# `bleeding_edge:` configuration adopts. The overlay is empty in this
|
|
18
|
+
# release, so the command currently reports an empty set; it becomes
|
|
19
|
+
# the inspection surface ADR-50 describes once a feature is queued.
|
|
20
|
+
#
|
|
21
|
+
# Read-only: it loads `.rigor.yml` to resolve the active selection
|
|
22
|
+
# but runs no analysis.
|
|
23
|
+
class ShowBleedingedgeCommand < Command
|
|
24
|
+
USAGE = "Usage: rigor show-bleedingedge [options]"
|
|
25
|
+
|
|
26
|
+
# @return [Integer] CLI exit status.
|
|
27
|
+
def run
|
|
28
|
+
options = parse_options
|
|
29
|
+
configuration = load_configuration(options)
|
|
30
|
+
return CLI::EXIT_USAGE if configuration.nil?
|
|
31
|
+
|
|
32
|
+
case options.fetch(:format)
|
|
33
|
+
when "json" then render_json(configuration)
|
|
34
|
+
else render_text(configuration)
|
|
35
|
+
end
|
|
36
|
+
0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def parse_options
|
|
42
|
+
options = { format: "text", config: nil }
|
|
43
|
+
OptionParser.new do |opt|
|
|
44
|
+
opt.banner = USAGE
|
|
45
|
+
opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
|
|
46
|
+
options[:format] = fmt
|
|
47
|
+
end
|
|
48
|
+
opt.on("--config=PATH", "Path to a .rigor.yml (default: auto-discovery).") do |path|
|
|
49
|
+
options[:config] = path
|
|
50
|
+
end
|
|
51
|
+
end.parse!(@argv)
|
|
52
|
+
options
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_configuration(options)
|
|
56
|
+
Configuration.load(options.fetch(:config))
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
@err.puts("show-bleedingedge: could not load configuration: #{e.message}")
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_json(configuration)
|
|
63
|
+
selector = configuration.bleeding_edge
|
|
64
|
+
@out.puts(JSON.pretty_generate(
|
|
65
|
+
"overlay" => BleedingEdge.features.map(&:to_h),
|
|
66
|
+
"selector" => configuration.to_h.fetch("bleeding_edge"),
|
|
67
|
+
"active" => BleedingEdge.active_features(selector).map(&:id),
|
|
68
|
+
"unknown_selected" => BleedingEdge.unknown_selected_ids(selector)
|
|
69
|
+
))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_text(configuration)
|
|
73
|
+
@out.puts("Bleeding-edge overlay (ADR-50 § WD2)")
|
|
74
|
+
@out.puts("")
|
|
75
|
+
if BleedingEdge.features.empty?
|
|
76
|
+
render_empty_overlay
|
|
77
|
+
else
|
|
78
|
+
render_overlay
|
|
79
|
+
end
|
|
80
|
+
@out.puts("")
|
|
81
|
+
render_selection(configuration)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_empty_overlay
|
|
85
|
+
@out.puts("The overlay is empty in this release — no features are queued for")
|
|
86
|
+
@out.puts("the next major. The `bleeding_edge:` mechanism is wired and ready;")
|
|
87
|
+
@out.puts("there is simply nothing to adopt yet.")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_overlay
|
|
91
|
+
@out.puts("#{BleedingEdge.features.length} feature(s) queued for the next major:")
|
|
92
|
+
@out.puts("")
|
|
93
|
+
BleedingEdge.features.each do |feature|
|
|
94
|
+
@out.puts(" #{feature.id}")
|
|
95
|
+
@out.puts(" #{feature.summary}")
|
|
96
|
+
feature.severity_overrides.each do |rule, severity|
|
|
97
|
+
@out.puts(" severity: #{rule} → :#{severity}")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_selection(configuration)
|
|
103
|
+
selector = configuration.bleeding_edge
|
|
104
|
+
active = BleedingEdge.active_features(selector).map(&:id)
|
|
105
|
+
@out.puts("Your configuration adopts: #{active.empty? ? '(none)' : active.join(', ')}")
|
|
106
|
+
|
|
107
|
+
unknown = BleedingEdge.unknown_selected_ids(selector)
|
|
108
|
+
return if unknown.empty?
|
|
109
|
+
|
|
110
|
+
@out.puts("Selected but not in this overlay (ignored): #{unknown.join(', ')}")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "options"
|
|
6
7
|
require_relative "../sig_gen"
|
|
7
8
|
require_relative "command"
|
|
8
9
|
|
|
@@ -145,7 +146,7 @@ module Rigor
|
|
|
145
146
|
opts.on("--tighter-returns", "Emit only tighter-return classifications") do
|
|
146
147
|
options[:selection] << SigGen::Classification::TIGHTER_RETURN
|
|
147
148
|
end
|
|
148
|
-
|
|
149
|
+
Options.add_config(opts, options)
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
@@ -5,6 +5,7 @@ require "optionparser"
|
|
|
5
5
|
require "prism"
|
|
6
6
|
|
|
7
7
|
require_relative "../configuration"
|
|
8
|
+
require_relative "options"
|
|
8
9
|
require_relative "../environment"
|
|
9
10
|
require_relative "../scope"
|
|
10
11
|
require_relative "../inference/flow_tracer"
|
|
@@ -63,7 +64,7 @@ module Rigor
|
|
|
63
64
|
options[:line] = value
|
|
64
65
|
end
|
|
65
66
|
opts.on("--verbose", "Include every expression enter/result frame") { options[:verbose] = true }
|
|
66
|
-
|
|
67
|
+
Options.add_config(opts, options)
|
|
67
68
|
end
|
|
68
69
|
parser.parse!(@argv)
|
|
69
70
|
options
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "options"
|
|
6
7
|
require_relative "../analysis/runner"
|
|
7
8
|
require_relative "../cache/store"
|
|
8
9
|
require_relative "../triage"
|
|
@@ -21,7 +22,7 @@ module Rigor
|
|
|
21
22
|
# command, not a gate (`rigor check` remains the gate).
|
|
22
23
|
class TriageCommand < Command
|
|
23
24
|
USAGE = "Usage: rigor triage [options] [paths]"
|
|
24
|
-
DEFAULT_SECTIONS = %i[distribution hotspots hints].freeze
|
|
25
|
+
DEFAULT_SECTIONS = %i[distribution selectors hotspots hints].freeze
|
|
25
26
|
|
|
26
27
|
# @return [Integer] CLI exit status (always 0).
|
|
27
28
|
def run
|
|
@@ -42,12 +43,15 @@ module Rigor
|
|
|
42
43
|
options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS }
|
|
43
44
|
OptionParser.new do |opts|
|
|
44
45
|
opts.banner = USAGE
|
|
45
|
-
|
|
46
|
+
Options.add_config(opts, options)
|
|
46
47
|
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
47
48
|
opts.on("--top=N", Integer, "Hotspot-file count (default 10)") { |v| options[:top] = v }
|
|
48
49
|
opts.on("--hints-only", "Print only the heuristic-hints section") { options[:sections] = %i[hints] }
|
|
49
|
-
opts.on("--no-hints", "Print distribution + hotspots only") do
|
|
50
|
-
options[:sections] = %i[distribution hotspots]
|
|
50
|
+
opts.on("--no-hints", "Print distribution + selectors + hotspots only") do
|
|
51
|
+
options[:sections] = %i[distribution selectors hotspots]
|
|
52
|
+
end
|
|
53
|
+
opts.on("--selectors-only", "Print only the class/method selectors section") do
|
|
54
|
+
options[:sections] = %i[selectors]
|
|
51
55
|
end
|
|
52
56
|
end.parse!(@argv)
|
|
53
57
|
validate!(options)
|
|
@@ -10,10 +10,11 @@ module Rigor
|
|
|
10
10
|
# triage` text report or as `--format json`.
|
|
11
11
|
class TriageRenderer
|
|
12
12
|
BAR_WIDTH = 24
|
|
13
|
+
SELECTOR_ROWS = 15 # text-output cap; `--format json` carries the full list
|
|
13
14
|
|
|
14
15
|
def initialize(report, sections:)
|
|
15
16
|
@report = report
|
|
16
|
-
@sections = sections # subset of %i[distribution hotspots hints]
|
|
17
|
+
@sections = sections # subset of %i[distribution selectors hotspots hints]
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def json
|
|
@@ -23,6 +24,7 @@ module Rigor
|
|
|
23
24
|
def text
|
|
24
25
|
blocks = []
|
|
25
26
|
blocks << distribution_block if @sections.include?(:distribution)
|
|
27
|
+
blocks << selectors_block if @sections.include?(:selectors)
|
|
26
28
|
blocks << hotspots_block if @sections.include?(:hotspots)
|
|
27
29
|
blocks << hints_block if @sections.include?(:hints)
|
|
28
30
|
"#{blocks.join("\n\n")}\n"
|
|
@@ -42,6 +44,18 @@ module Rigor
|
|
|
42
44
|
lines.join("\n")
|
|
43
45
|
end
|
|
44
46
|
|
|
47
|
+
def selectors_block
|
|
48
|
+
return "Selectors — by class / method\n (none)" if @report.selectors.empty?
|
|
49
|
+
|
|
50
|
+
lines = ["Selectors — by class / method (top #{SELECTOR_ROWS}; full list in --format json)"]
|
|
51
|
+
@report.selectors.first(SELECTOR_ROWS).each do |sel|
|
|
52
|
+
label = sel.receiver ? "#{sel.receiver}##{sel.method_name}" : sel.method_name
|
|
53
|
+
lines << format(" %<label>-44s %<count>5d %<files>3d file(s)",
|
|
54
|
+
label: label, count: sel.count, files: sel.files)
|
|
55
|
+
end
|
|
56
|
+
lines.join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
45
59
|
def hotspots_block
|
|
46
60
|
return "Hotspot files\n (none)" if @report.hotspots.empty?
|
|
47
61
|
|
|
@@ -53,7 +53,7 @@ module Rigor
|
|
|
53
53
|
opts.banner = USAGE
|
|
54
54
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
55
55
|
opts.on("--trace", "Record fail-soft fallbacks via FallbackTracer") { options[:trace] = true }
|
|
56
|
-
|
|
56
|
+
Options.add_config(opts, options)
|
|
57
57
|
Options.add_editor_mode(opts, options)
|
|
58
58
|
end
|
|
59
59
|
parser.parse!(@argv)
|
|
@@ -4,6 +4,7 @@ require "optionparser"
|
|
|
4
4
|
require "prism"
|
|
5
5
|
|
|
6
6
|
require_relative "../configuration"
|
|
7
|
+
require_relative "options"
|
|
7
8
|
require_relative "../environment"
|
|
8
9
|
require_relative "../inference/coverage_scanner"
|
|
9
10
|
require_relative "../scope"
|
|
@@ -46,7 +47,7 @@ module Rigor
|
|
|
46
47
|
parser = OptionParser.new do |opts|
|
|
47
48
|
opts.banner = USAGE
|
|
48
49
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
49
|
-
|
|
50
|
+
Options.add_config(opts, options)
|
|
50
51
|
opts.on("--limit=N", Integer, "Max example events to print (text only)") do |value|
|
|
51
52
|
options[:limit] = value
|
|
52
53
|
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -38,7 +38,8 @@ module Rigor
|
|
|
38
38
|
"plugins" => :run_plugins,
|
|
39
39
|
"plugin" => :run_plugin,
|
|
40
40
|
"playground" => :run_playground,
|
|
41
|
-
"skill" => :run_skill
|
|
41
|
+
"skill" => :run_skill,
|
|
42
|
+
"show-bleedingedge" => :run_show_bleedingedge
|
|
42
43
|
}.freeze
|
|
43
44
|
|
|
44
45
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -150,8 +151,9 @@ module Rigor
|
|
|
150
151
|
# - target_ruby: minimum Ruby version your project targets.
|
|
151
152
|
# - paths: directories scanned by `rigor check` and
|
|
152
153
|
# `rigor type-scan` when no path is given.
|
|
153
|
-
# - plugins:
|
|
154
|
-
#
|
|
154
|
+
# - plugins: opt-in list of plugin gem names to load.
|
|
155
|
+
# See https://github.com/rigortype/rigor/tree/main/plugins
|
|
156
|
+
# for production plugins (rigor-activerecord, rigor-sorbet, …).
|
|
155
157
|
# - disable: list of `rigor check` rule identifiers to
|
|
156
158
|
# silence project-wide. The shipped rules are
|
|
157
159
|
# call.undefined-method, call.wrong-arity,
|
|
@@ -289,6 +291,12 @@ module Rigor
|
|
|
289
291
|
CLI::PluginCommand.new(argv: @argv, out: @out, err: @err).run
|
|
290
292
|
end
|
|
291
293
|
|
|
294
|
+
def run_show_bleedingedge
|
|
295
|
+
require_relative "cli/show_bleedingedge_command"
|
|
296
|
+
|
|
297
|
+
CLI::ShowBleedingedgeCommand.new(argv: @argv, out: @out, err: @err).run
|
|
298
|
+
end
|
|
299
|
+
|
|
292
300
|
def help
|
|
293
301
|
<<~HELP
|
|
294
302
|
Usage: rigor <command> [options]
|
|
@@ -311,6 +319,7 @@ module Rigor
|
|
|
311
319
|
plugin Browse bundled plugin source as worked examples (list/path/print/root)
|
|
312
320
|
playground Start the browser playground (requires rigor-playground gem)
|
|
313
321
|
skill List or print bundled Agent Skills (rigor-project-init, ...)
|
|
322
|
+
show-bleedingedge Show the bleeding-edge overlay + what your config adopts (ADR-50)
|
|
314
323
|
version Print the Rigor version
|
|
315
324
|
help Print this help
|
|
316
325
|
HELP
|
|
@@ -9,10 +9,8 @@ module Rigor
|
|
|
9
9
|
# inference instead of degrading to `Dynamic[top]` at the
|
|
10
10
|
# dependency boundary.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# wires `Analysis::DependencySourceInference` against this
|
|
15
|
-
# value object.
|
|
12
|
+
# Parser for the `dependencies:` YAML section; consumed by
|
|
13
|
+
# `Analysis::DependencySourceInference` (ADR-10).
|
|
16
14
|
class Dependencies
|
|
17
15
|
# Walking modes per
|
|
18
16
|
# [ADR-10 § "Decision"](../../../docs/adr/10-dependency-source-inference.md#decision).
|
|
@@ -130,14 +130,26 @@ module Rigor
|
|
|
130
130
|
# Keys are canonical rule ids; values are
|
|
131
131
|
# {VALID_SEVERITIES} symbols. Family-wildcard keys
|
|
132
132
|
# (`call`) match every rule under that prefix.
|
|
133
|
+
# @param bleeding_edge_overrides [Hash{String => Symbol}] the
|
|
134
|
+
# severity map imposed by the active ADR-50 § WD2 bleeding-edge
|
|
135
|
+
# features ({Rigor::BleedingEdge.severity_overrides_for}).
|
|
136
|
+
# Consulted *below* the user's own `overrides` (so an explicit
|
|
137
|
+
# `severity_overrides:` entry, exact or family wildcard, always
|
|
138
|
+
# wins) and *above* the profile table. Exact rule ids only — the
|
|
139
|
+
# overlay never carries family wildcards. Empty while the
|
|
140
|
+
# overlay is unpopulated, so the default leaves resolution
|
|
141
|
+
# bit-for-bit unchanged.
|
|
133
142
|
# @return [Symbol] the resolved severity. Returns `:off` to
|
|
134
143
|
# mean "drop the diagnostic entirely".
|
|
135
|
-
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {})
|
|
144
|
+
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {}, bleeding_edge_overrides: {})
|
|
136
145
|
return authored_severity if rule.nil?
|
|
137
146
|
|
|
138
147
|
override = overrides[rule] || family_override(rule, overrides)
|
|
139
148
|
return override.to_sym if override
|
|
140
149
|
|
|
150
|
+
bleeding = bleeding_edge_overrides[rule]
|
|
151
|
+
return bleeding.to_sym if bleeding
|
|
152
|
+
|
|
141
153
|
profile_table = PROFILES[profile] || PROFILES.fetch(DEFAULT_PROFILE)
|
|
142
154
|
profile_table.fetch(rule, authored_severity)
|
|
143
155
|
end
|