rigortype 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +147 -10
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- metadata +69 -3
|
@@ -4,7 +4,11 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
6
|
require_relative "../environment"
|
|
7
|
+
require_relative "../inference/parameter_inference_collector"
|
|
7
8
|
require_relative "../inference/precision_scanner"
|
|
9
|
+
require_relative "../inference/scope_indexer"
|
|
10
|
+
require_relative "../language_server/project_context"
|
|
11
|
+
require_relative "../protection/discovery_seed"
|
|
8
12
|
require_relative "../scope"
|
|
9
13
|
require_relative "coverage_report"
|
|
10
14
|
|
|
@@ -20,19 +24,56 @@ module Rigor
|
|
|
20
24
|
# @param files [Array<String>] explicit `.rb` file paths to scan.
|
|
21
25
|
# @param configuration [Rigor::Configuration]
|
|
22
26
|
# @return [Rigor::CLI::CoverageReport]
|
|
27
|
+
#
|
|
28
|
+
# Issue #513 — the environment is PLUGIN-AWARE (`LanguageServer::ProjectContext`, the same builder
|
|
29
|
+
# `--protection` uses), not the bare RBS environment this path carried until 2026-09-01. Without the
|
|
30
|
+
# plugin registry every plugin-typed receiver read `Dynamic` and the precision ratio understated the
|
|
31
|
+
# engine by ~1.2 points on a Rails target — the defect the protection path documented and fixed for
|
|
32
|
+
# itself on 2026-07-04, never applied here.
|
|
23
33
|
def precision_report(files:, configuration:)
|
|
24
|
-
scope =
|
|
34
|
+
scope = discovery_seeded_scope(
|
|
35
|
+
files: files,
|
|
36
|
+
configuration: configuration,
|
|
37
|
+
environment: LanguageServer::ProjectContext.new(configuration: configuration).environment,
|
|
38
|
+
parameter_inference: configuration.parameter_inference
|
|
39
|
+
)
|
|
25
40
|
scanner = Inference::PrecisionScanner.new(scope: scope)
|
|
26
41
|
accumulator = CoverageAccumulator.new
|
|
27
42
|
files.each { |path| scan_into(path, scanner, accumulator, configuration) }
|
|
28
43
|
accumulator.to_report(files, {})
|
|
29
44
|
end
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
# The cross-file facts a scan must see to report the types the engine actually infers, rather than the types a
|
|
47
|
+
# file-at-a-time walk can reach on its own.
|
|
48
|
+
#
|
|
49
|
+
# Issue #513 — the seed is the FULL check-walk bundle (`Protection::DiscoverySeed.discovery_tables`: classes,
|
|
50
|
+
# def nodes and sources for both kinds, superclasses, includes, visibilities, methods, and the Data / Struct
|
|
51
|
+
# member layouts), not just `discovered_classes`. The two-table seed #505 introduced closed the biggest part of
|
|
52
|
+
# the gap and left the rest: without the def-node / ancestry tables every cross-file call to a source-inferred
|
|
53
|
+
# project method measured as unresolved while `rigor check` resolves it — +0.27pp on redmine, +0.77pp on
|
|
54
|
+
# mastodon from the tables alone.
|
|
55
|
+
#
|
|
56
|
+
# `parameter_inference` is a parameter rather than a config read because the two callers want different answers:
|
|
57
|
+
# the precision lens must mirror the walk it describes (`check` leaves the table empty unless
|
|
58
|
+
# `parameter_inference:` is on, ADR-67 WD6a), while `--protection` seeds it unconditionally by ADR-67's own
|
|
59
|
+
# wiring — it only ever reclassifies sites it already counted.
|
|
60
|
+
#
|
|
61
|
+
# @return [Rigor::Scope]
|
|
62
|
+
def discovery_seeded_scope(files:, configuration:, environment:, parameter_inference:, workers: nil)
|
|
63
|
+
base = Scope.empty(environment: environment)
|
|
64
|
+
seed = Protection::DiscoverySeed.discovery_tables(files).dup
|
|
65
|
+
|
|
66
|
+
if parameter_inference
|
|
67
|
+
table = Inference::ParameterInferenceCollector.collect(
|
|
68
|
+
files: files, environment: environment, target_ruby: configuration.target_ruby,
|
|
69
|
+
workers: workers || configuration.parallel_workers
|
|
70
|
+
)
|
|
71
|
+
seed[:param_inferred_types] = table unless table.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return base if seed.empty?
|
|
75
|
+
|
|
76
|
+
base.with_discovery(base.discovery.with(**seed))
|
|
36
77
|
end
|
|
37
78
|
|
|
38
79
|
# Parses one file and feeds the scan result (or a parse-error record) into `accumulator`. `scanner` /
|
|
@@ -27,17 +27,34 @@ module Rigor
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def render_text(report)
|
|
30
|
-
pct = (report.ratio * 100).round(1)
|
|
30
|
+
pct = report.ratio ? (report.ratio * 100).round(1) : nil
|
|
31
31
|
@out.puts "Fused protection (static type ∪ dynamic test)"
|
|
32
|
-
@out.puts " protected: #{report.protected_total} / #{report.grand_total}
|
|
32
|
+
@out.puts " protected: #{report.protected_total} / #{report.grand_total}#{ratio_suffix(pct)}"
|
|
33
33
|
@out.puts " by type: #{report.total_type_killed}"
|
|
34
34
|
@out.puts " by test: #{report.total_test_killed} (type-survivors a test caught)"
|
|
35
35
|
@out.puts " unprotected: #{report.total_unprotected} (neither — add a type or a test)"
|
|
36
|
+
render_unmeasured_files(report)
|
|
36
37
|
render_harness_errors(report)
|
|
37
38
|
render_unprotected(report)
|
|
38
39
|
render_files(report)
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
# Issue #686 — see {MutationProtectionRenderer#ratio_suffix}.
|
|
43
|
+
def ratio_suffix(pct)
|
|
44
|
+
return " (not measured)" if pct.nil?
|
|
45
|
+
|
|
46
|
+
" (#{pct}%)"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Issue #686 — see {MutationProtectionRenderer#render_unmeasured_files}.
|
|
50
|
+
def render_unmeasured_files(report)
|
|
51
|
+
count = report.unmeasured_files
|
|
52
|
+
return if count.zero?
|
|
53
|
+
|
|
54
|
+
@out.puts " unmeasured files: #{count} — every mutation of them failed inside the measurement " \
|
|
55
|
+
"harness, so they are not in the ratio above"
|
|
56
|
+
end
|
|
57
|
+
|
|
41
58
|
# #264 — see {MutationProtectionRenderer#render_harness_errors}; surfaced only when non-zero.
|
|
42
59
|
def render_harness_errors(report)
|
|
43
60
|
count = report.total_harness_errors
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../protection/measurement_integrity"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class CLI
|
|
5
7
|
# ADR-70 — aggregates per-file {Protection::MutationScanner::FusedFileResult} into a project-level **fused**
|
|
@@ -14,6 +16,13 @@ module Rigor
|
|
|
14
16
|
def initialize(path:, type_killed:, test_killed:, unprotected:, ratio:, harness_errors: 0)
|
|
15
17
|
super
|
|
16
18
|
end
|
|
19
|
+
|
|
20
|
+
# Issue #686 — see {Rigor::CLI::FileEffectiveness#unmeasured?}.
|
|
21
|
+
def unmeasured?
|
|
22
|
+
!Protection::MeasurementIntegrity.measured?(
|
|
23
|
+
total: type_killed + test_killed + unprotected, harness_errors: harness_errors
|
|
24
|
+
)
|
|
25
|
+
end
|
|
17
26
|
end
|
|
18
27
|
UnprotectedBreakage = Data.define(:method_name, :count, :examples)
|
|
19
28
|
|
|
@@ -23,19 +32,31 @@ module Rigor
|
|
|
23
32
|
def total_unprotected = files.sum(&:unprotected)
|
|
24
33
|
def grand_total = total_type_killed + total_test_killed + total_unprotected
|
|
25
34
|
def protected_total = total_type_killed + total_test_killed
|
|
26
|
-
|
|
35
|
+
|
|
36
|
+
# Issue #686 review (second pass) — see {MutationProtectionReport#ratio}.
|
|
37
|
+
def ratio
|
|
38
|
+
return nil if Protection::MeasurementIntegrity.ratio_unmeasurable?(
|
|
39
|
+
grand_total: grand_total, unmeasured_files: unmeasured_files
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
grand_total.zero? ? 1.0 : protected_total.to_f / grand_total
|
|
43
|
+
end
|
|
27
44
|
|
|
28
45
|
# #264 — stays OUT of `grand_total`/`ratio`, exactly like the plain mutation report.
|
|
29
46
|
def total_harness_errors = files.sum(&:harness_errors)
|
|
30
47
|
|
|
48
|
+
# Issue #686 — see {Rigor::CLI::MutationProtectionReport#unmeasured_files}.
|
|
49
|
+
def unmeasured_files = files.count(&:unmeasured?)
|
|
50
|
+
|
|
31
51
|
def to_h
|
|
32
52
|
{
|
|
33
53
|
"mode" => "protection-fused",
|
|
34
54
|
"type_killed" => total_type_killed,
|
|
35
55
|
"test_killed" => total_test_killed,
|
|
36
56
|
"unprotected" => total_unprotected,
|
|
37
|
-
"protected_ratio" => ratio
|
|
57
|
+
"protected_ratio" => ratio&.round(4),
|
|
38
58
|
"harness_errors" => total_harness_errors,
|
|
59
|
+
"unmeasured_files" => unmeasured_files,
|
|
39
60
|
"files" => files.map do |f|
|
|
40
61
|
{ "path" => f.path, "type_killed" => f.type_killed, "test_killed" => f.test_killed,
|
|
41
62
|
"unprotected" => f.unprotected, "ratio" => f.ratio.round(4), "harness_errors" => f.harness_errors }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# The stderr half of "did this measurement actually happen" for `rigor coverage --protection --mutation`.
|
|
6
|
+
#
|
|
7
|
+
# Two counts, escalating, and the difference between them is the whole point:
|
|
8
|
+
#
|
|
9
|
+
# - `harness_errors` (#264) counts MUTANTS the harness rescued. Advisory and floored — a few are the
|
|
10
|
+
# transient the bucket exists to make visible, and `determine_protection_exit` deliberately ignores
|
|
11
|
+
# them so a `--threshold` build does not start failing for a reason unrelated to the ratio it pins.
|
|
12
|
+
# - `unmeasured_files` (#686) counts FILES where nothing could be measured at all, because the clean
|
|
13
|
+
# baseline itself came back from a crashed analysis. No floor and not advisory: the ratio then answers
|
|
14
|
+
# a smaller question than the user asked, `determine_protection_exit` fails the run, and this says so.
|
|
15
|
+
# Without it, `killed + survived == 0` borrowed the vacuous-file convention's 1.0 and a run that
|
|
16
|
+
# measured nothing printed 100% and passed every threshold.
|
|
17
|
+
#
|
|
18
|
+
# Extracted from {CoverageMutation} rather than inlined there: that module sits at its length budget, and
|
|
19
|
+
# a warning pair with its own escalation rule is a thing worth naming anyway.
|
|
20
|
+
module MeasurementIntegrityWarning
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @param report [MutationProtectionReport, FusedProtectionReport] both expose the two counts.
|
|
24
|
+
# @param err [IO] the stream to warn on.
|
|
25
|
+
# @param floor [Integer] {CoverageMutation::HARNESS_ERROR_WARN_FLOOR}.
|
|
26
|
+
# @return [void]
|
|
27
|
+
def emit(report, err:, floor:)
|
|
28
|
+
lines_for(report, floor: floor).each { |line| err.puts(line) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Array<String>] the warnings this report earns, widest consequence first.
|
|
32
|
+
def lines_for(report, floor:)
|
|
33
|
+
lines = []
|
|
34
|
+
unmeasured = report.respond_to?(:unmeasured_files) ? report.unmeasured_files : 0 # see CoverageCommand
|
|
35
|
+
lines << unmeasured_line(unmeasured) if unmeasured.positive?
|
|
36
|
+
harness_errors = report.total_harness_errors
|
|
37
|
+
lines << harness_error_line(harness_errors, floor) if harness_errors >= floor
|
|
38
|
+
lines
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unmeasured_line(count)
|
|
42
|
+
"coverage: #{count} file(s) could not be measured at all — every mutation of them failed inside " \
|
|
43
|
+
"the measurement harness, typically because Rigor's own analysis crashed while re-analysing " \
|
|
44
|
+
"them. They contribute nothing to the ratio, so the effectiveness above is measured over the " \
|
|
45
|
+
"REST of the project. Exiting non-zero: a ratio over part of what you asked about must not read " \
|
|
46
|
+
"as a pass."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def harness_error_line(count, floor)
|
|
50
|
+
"coverage: #{count} mutants failed inside the measurement harness (\"harness_errors\", at/above " \
|
|
51
|
+
"the #{floor}-mutant floor) — excluded from the ratio like a parse-invalid mutant, but this many " \
|
|
52
|
+
"suggests a harness defect rather than one-off noise. Investigate before trusting --threshold " \
|
|
53
|
+
"on this run."
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -26,15 +26,35 @@ module Rigor
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def render_text(report)
|
|
29
|
-
pct = (report.ratio * 100).round(1)
|
|
29
|
+
pct = report.ratio ? (report.ratio * 100).round(1) : nil
|
|
30
30
|
@out.puts "Type-protection effectiveness (Tier 2 — mutation kill rate)"
|
|
31
|
-
@out.puts " caught breakages: #{report.total_killed} / #{report.grand_total}
|
|
31
|
+
@out.puts " caught breakages: #{report.total_killed} / #{report.grand_total}#{ratio_suffix(pct)}"
|
|
32
32
|
@out.puts " (effectiveness = when a type-visible bug was introduced, Rigor caught it)"
|
|
33
|
+
render_unmeasured_files(report)
|
|
33
34
|
render_harness_errors(report)
|
|
34
35
|
render_missed(report)
|
|
35
36
|
render_files(report)
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Issue #686 — `0 / 0` is `ratio` 1.0 by the vacuous-file convention, and printing "100.0%" over a run
|
|
40
|
+
# that measured nothing is the exact shape this issue exists to end. When nothing was measured AND a
|
|
41
|
+
# file went unmeasured, the percentage is withheld rather than invented.
|
|
42
|
+
def ratio_suffix(pct)
|
|
43
|
+
return " (not measured)" if pct.nil?
|
|
44
|
+
|
|
45
|
+
" (#{pct}%)"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Issue #686 — the count that makes the command exit non-zero, on stdout beside the ratio it qualifies
|
|
49
|
+
# (the stderr twin in `CoverageMutation#warn_unmeasured_files` carries the explanation).
|
|
50
|
+
def render_unmeasured_files(report)
|
|
51
|
+
count = report.unmeasured_files
|
|
52
|
+
return if count.zero?
|
|
53
|
+
|
|
54
|
+
@out.puts " unmeasured files: #{count} — every mutation of them failed inside the measurement " \
|
|
55
|
+
"harness, so they are not in the ratio above"
|
|
56
|
+
end
|
|
57
|
+
|
|
38
58
|
# #264 — surfaced only when non-zero: a harness-level failure is a defect in the measurement itself, not
|
|
39
59
|
# in the code being measured, and a clean run should not carry a permanent line about a bucket that is
|
|
40
60
|
# always empty.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../protection/measurement_integrity"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class CLI
|
|
5
7
|
# ADR-63 Tier 2 — aggregates per-file {Protection::MutationScanner} results into a project-level *effectiveness*
|
|
@@ -16,6 +18,13 @@ module Rigor
|
|
|
16
18
|
def initialize(path:, killed:, survived:, ratio:, harness_errors: 0)
|
|
17
19
|
super
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
# Issue #686 — the file measured the HARNESS, not the code: every mutant it had landed in
|
|
23
|
+
# `harness_errors`, so `killed + survived` is zero for a reason that is not "there was nothing to
|
|
24
|
+
# measure". `ratio` is 0.0 here rather than the vacuous 1.0, and this is what the exit gate reads.
|
|
25
|
+
def unmeasured?
|
|
26
|
+
!Protection::MeasurementIntegrity.measured?(total: killed + survived, harness_errors: harness_errors)
|
|
27
|
+
end
|
|
19
28
|
end
|
|
20
29
|
MissedBreakage = Data.define(:method_name, :count, :examples)
|
|
21
30
|
|
|
@@ -23,21 +32,41 @@ module Rigor
|
|
|
23
32
|
def total_killed = files.sum(&:killed)
|
|
24
33
|
def total_survived = files.sum(&:survived)
|
|
25
34
|
def grand_total = total_killed + total_survived
|
|
26
|
-
|
|
35
|
+
|
|
36
|
+
# Issue #686 review (second pass) — `nil`, not 1.0, when nothing was measured and something failed
|
|
37
|
+
# trying. The text renderer already withheld the percentage; JSON did not, so a CI gate reading
|
|
38
|
+
# `effectiveness_ratio` still saw 100% on a run that measured nothing — the same defect one channel
|
|
39
|
+
# over. An empty project (nothing to measure, nothing failed) stays vacuously 1.0.
|
|
40
|
+
def ratio
|
|
41
|
+
return nil if Protection::MeasurementIntegrity.ratio_unmeasurable?(
|
|
42
|
+
grand_total: grand_total, unmeasured_files: unmeasured_files
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
grand_total.zero? ? 1.0 : total_killed.to_f / grand_total
|
|
46
|
+
end
|
|
27
47
|
|
|
28
48
|
# #264 — a harness-level failure count, summed across files. Stays OUT of `grand_total`/`ratio` exactly
|
|
29
49
|
# like a parse error: it is not a measurement of the code.
|
|
30
50
|
def total_harness_errors = files.sum(&:harness_errors)
|
|
31
51
|
|
|
52
|
+
# Issue #686 — files where NOTHING could be measured. Distinct from `total_harness_errors`, which
|
|
53
|
+
# counts mutants and rightly tolerates a few (#264's floor): one wholly unmeasured file already means
|
|
54
|
+
# the ratio is computed over a smaller project than the user asked about, so this gates the exit and
|
|
55
|
+
# the mutant count does not.
|
|
56
|
+
def unmeasured_files = files.count(&:unmeasured?)
|
|
57
|
+
|
|
32
58
|
def to_h
|
|
33
59
|
{
|
|
34
60
|
"mode" => "mutation",
|
|
35
61
|
"killed" => total_killed,
|
|
36
62
|
"survived" => total_survived,
|
|
37
|
-
"effectiveness_ratio" => ratio
|
|
63
|
+
"effectiveness_ratio" => ratio&.round(4),
|
|
38
64
|
# #264 — unconditional: a JSON consumer (e.g. a CI gate) must be able to check this every run, not
|
|
39
65
|
# only when a text renderer decided it was worth a line.
|
|
40
66
|
"harness_errors" => total_harness_errors,
|
|
67
|
+
# Issue #686 — unconditional for the same reason `harness_errors` is: a CI gate reading JSON must
|
|
68
|
+
# be able to see that part of the project was never measured, on every run.
|
|
69
|
+
"unmeasured_files" => unmeasured_files,
|
|
41
70
|
"files" => files.map do |f|
|
|
42
71
|
{ "path" => f.path, "killed" => f.killed, "survived" => f.survived,
|
|
43
72
|
"ratio" => f.ratio.round(4), "harness_errors" => f.harness_errors }
|
|
@@ -55,6 +55,7 @@ module Rigor
|
|
|
55
55
|
0
|
|
56
56
|
end
|
|
57
57
|
report_unrenderable(generator.unrenderable)
|
|
58
|
+
report_unresolvable_superclasses(generator.unresolvable_superclasses)
|
|
58
59
|
status
|
|
59
60
|
end
|
|
60
61
|
|
|
@@ -79,6 +80,23 @@ module Rigor
|
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
|
|
83
|
+
# Issue #735 — unlike {#report_unrenderable} this is a fact about the PROJECT's type universe, not a
|
|
84
|
+
# Rigor defect: the class's superclass is not declared by any RBS the environment loads, so a sidecar
|
|
85
|
+
# declaring it would fail to build and take the class's type coverage with it. Naming the unresolved
|
|
86
|
+
# superclass is the actionable part — it is usually one gem's missing RBS standing between the user
|
|
87
|
+
# and signatures for a whole layer of their app.
|
|
88
|
+
def report_unresolvable_superclasses(unresolvable)
|
|
89
|
+
return if unresolvable.empty?
|
|
90
|
+
|
|
91
|
+
@err.puts(
|
|
92
|
+
"rigor sig-gen: skipped #{unresolvable.size} class(es) whose superclass no loaded RBS declares. " \
|
|
93
|
+
"A signature for them would fail to build (`RBS::NoSuperclassFoundError`) and would leave the " \
|
|
94
|
+
"class LESS typed than it is now. Provide RBS for the superclass — `rbs collection install`, the " \
|
|
95
|
+
"gem's own `sig/`, or a `signature_paths:` entry — and re-run."
|
|
96
|
+
)
|
|
97
|
+
unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
|
|
98
|
+
end
|
|
99
|
+
|
|
82
100
|
def dispatch_print_or_diff(candidates, mode, options)
|
|
83
101
|
SigGen::Renderer.new(out: @out).render(
|
|
84
102
|
candidates: candidates,
|