rigortype 0.1.19 → 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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- 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/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -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 +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -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 +2 -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 +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- 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 +49 -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/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 +20 -28
- 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/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- 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/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- 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 +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- 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 +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- 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 +14 -2
- data/lib/rigor/scope.rb +54 -11
- 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/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -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 +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- 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 +7 -9
- 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 +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- 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 +1 -1
- 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 +5 -5
- 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 +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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 +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Builtins
|
|
7
|
+
# Refined types for predefined Ruby / stdlib constants whose upstream
|
|
8
|
+
# RBS signatures are broader than the constants' documented runtime
|
|
9
|
+
# invariants.
|
|
10
|
+
#
|
|
11
|
+
# Resolution is two-tiered:
|
|
12
|
+
#
|
|
13
|
+
# **Tier 1 — exact-value whitelist** (`FOLDED_CONSTANTS`):
|
|
14
|
+
# Constants whose value is bit-for-bit identical across every Ruby
|
|
15
|
+
# version and platform are folded to `Constant[T]`: the `Math::PI`
|
|
16
|
+
# / `Math::E` math constants (C's `M_PI` / `M_E`) and the four
|
|
17
|
+
# IEEE 754 binary64 magnitude constants `Float::INFINITY` /
|
|
18
|
+
# `::MAX` / `::MIN` / `::EPSILON` (each a single format-mandated bit
|
|
19
|
+
# pattern). Add new entries only when the value is truly
|
|
20
|
+
# cross-implementation invariant AND compares reflexively under
|
|
21
|
+
# `==` — the latter is why `Float::NAN` is deliberately EXCLUDED:
|
|
22
|
+
# `NaN == NaN` is `false`, so a `Constant[NAN]` would violate the
|
|
23
|
+
# `Type::Constant` `==` / `eql?` / `hash` contract (it would hash
|
|
24
|
+
# equal to itself yet compare unequal), corrupting type-equality
|
|
25
|
+
# and union dedup. The binary64 *integer* shape parameters
|
|
26
|
+
# (`Float::DIG` / `MANT_DIG` / `MAX_EXP` / …) are intentionally NOT
|
|
27
|
+
# folded: upstream RBS hedges them as "Usually defaults to …", and
|
|
28
|
+
# as plain `Integer`s they fall through Tier 2 to the RBS type
|
|
29
|
+
# harmlessly. `Complex::I` is deferred (no complex-fold consumer).
|
|
30
|
+
#
|
|
31
|
+
# **Tier 2 — runtime String inspection**:
|
|
32
|
+
# For any other constant, the module resolves it via `const_get`
|
|
33
|
+
# against the analyzer's own Ruby runtime. Core / stdlib constants
|
|
34
|
+
# (e.g. `RUBY_VERSION`, `RUBY_PLATFORM`) are always loaded into the
|
|
35
|
+
# analyzer process; project-defined constants are not (they live only
|
|
36
|
+
# in ASTs), so their `const_get` raises `NameError` and the lookup
|
|
37
|
+
# falls through to the RBS type tier.
|
|
38
|
+
#
|
|
39
|
+
# For a successfully resolved `String` value:
|
|
40
|
+
# - empty string → no refinement (fall through to RBS `String`)
|
|
41
|
+
# - a Ruby numeric literal → `numeric-string`
|
|
42
|
+
# - non-empty otherwise → `non-empty-string`
|
|
43
|
+
#
|
|
44
|
+
# **Exclusion set** (`RUNTIME_INSPECTION_EXCLUDED`):
|
|
45
|
+
# String constants that appear non-empty in the current runtime but
|
|
46
|
+
# are documented to be potentially empty in some build configuration
|
|
47
|
+
# or alternative implementation. Exclusions are populated by
|
|
48
|
+
# scanning Ruby's C source (version.c, etc.) and RBS comments for
|
|
49
|
+
# any constant whose documentation says "may be empty" or
|
|
50
|
+
# "platform-specific default". None are known today; the set
|
|
51
|
+
# exists as a safety net.
|
|
52
|
+
#
|
|
53
|
+
# This module is consulted by `Environment#constant_for_name` BEFORE
|
|
54
|
+
# the RBS constant-type table (widest types) but AFTER in-source
|
|
55
|
+
# constant writes (the user's own `Math::PI = 0.0` takes precedence
|
|
56
|
+
# via the lexical-candidate walk in `ExpressionTyper`).
|
|
57
|
+
module PredefinedConstantRefinements
|
|
58
|
+
# --- tier 1 -------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Exact-value fold whitelist. Keys are unqualified constant paths
|
|
61
|
+
# (no leading "::") matching what `Environment#constant_for_name`
|
|
62
|
+
# receives.
|
|
63
|
+
FOLDED_CONSTANTS = {
|
|
64
|
+
# Math module — IEEE 754 bit-identical across all MRI / JRuby /
|
|
65
|
+
# TruffleRuby builds; folding enables precise constant arithmetic.
|
|
66
|
+
"Math::PI" => Type::Combinator.constant_of(::Math::PI).freeze,
|
|
67
|
+
"Math::E" => Type::Combinator.constant_of(::Math::E).freeze,
|
|
68
|
+
|
|
69
|
+
# Float magnitude limits — each a single format-mandated IEEE 754
|
|
70
|
+
# binary64 bit pattern (`+Inf`, `DBL_MAX`, `DBL_MIN`,
|
|
71
|
+
# `DBL_EPSILON`), reflexive under `==`. `Float::NAN` is excluded
|
|
72
|
+
# (non-reflexive `==` — see the module-level note).
|
|
73
|
+
"Float::INFINITY" => Type::Combinator.constant_of(::Float::INFINITY).freeze,
|
|
74
|
+
"Float::MAX" => Type::Combinator.constant_of(::Float::MAX).freeze,
|
|
75
|
+
"Float::MIN" => Type::Combinator.constant_of(::Float::MIN).freeze,
|
|
76
|
+
"Float::EPSILON" => Type::Combinator.constant_of(::Float::EPSILON).freeze
|
|
77
|
+
}.freeze
|
|
78
|
+
private_constant :FOLDED_CONSTANTS
|
|
79
|
+
|
|
80
|
+
# --- tier 2 -------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
# String constants whose runtime value is non-empty in the current
|
|
83
|
+
# Ruby but that should NOT be narrowed because they are documented
|
|
84
|
+
# to be potentially empty in some build or implementation.
|
|
85
|
+
#
|
|
86
|
+
# Methodology: grep Ruby's version.c and similar C sources, and the
|
|
87
|
+
# RBS comment corpus, for any constant annotated with "may be empty"
|
|
88
|
+
# or "platform-specific default". Add the full qualified path
|
|
89
|
+
# (without leading "::") when a genuine risk is found.
|
|
90
|
+
RUNTIME_INSPECTION_EXCLUDED = Set[].freeze
|
|
91
|
+
private_constant :RUNTIME_INSPECTION_EXCLUDED
|
|
92
|
+
|
|
93
|
+
NON_EMPTY_STRING = Type::Combinator.non_empty_string.freeze
|
|
94
|
+
NUMERIC_STRING = Type::Combinator.numeric_string.freeze
|
|
95
|
+
private_constant :NON_EMPTY_STRING, :NUMERIC_STRING
|
|
96
|
+
|
|
97
|
+
# --- public API ---------------------------------------------------
|
|
98
|
+
|
|
99
|
+
# @param name [String] unqualified constant name (e.g. `"Math::PI"`,
|
|
100
|
+
# `"RUBY_VERSION"`, `"Ruby::ENGINE"`)
|
|
101
|
+
# @return [Rigor::Type, nil] refined type, or nil to fall through
|
|
102
|
+
def self.lookup(name)
|
|
103
|
+
FOLDED_CONSTANTS[name] || inspect_runtime_string(name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- private ------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
# Resolves `name` via `const_get` in the analyzer's runtime and
|
|
109
|
+
# returns a refined String carrier, or nil.
|
|
110
|
+
def self.inspect_runtime_string(name)
|
|
111
|
+
return nil if RUNTIME_INSPECTION_EXCLUDED.include?(name)
|
|
112
|
+
|
|
113
|
+
mod = ::Object
|
|
114
|
+
name.split("::").each do |part|
|
|
115
|
+
# Resolve only constants already present — never let analysing a
|
|
116
|
+
# reference drive the analyzer's own runtime to autoload or run a
|
|
117
|
+
# `const_missing` hook. A `Digest::UUID` reference in project code
|
|
118
|
+
# otherwise makes `const_get` trigger `Digest.const_missing` →
|
|
119
|
+
# `require "digest/uuid"`, and a missing optional library raises
|
|
120
|
+
# `LoadError` (a `ScriptError`, not the `NameError` the const_get
|
|
121
|
+
# walk expects), which would abort the whole run rather than fall
|
|
122
|
+
# through to the RBS tier. `const_defined?(part, false)` answers
|
|
123
|
+
# the same "is this resolvable here" question without the side
|
|
124
|
+
# effect — a project-defined constant (the common case) is simply
|
|
125
|
+
# absent and returns nil, no exception raised.
|
|
126
|
+
return nil unless mod.is_a?(::Module) && mod.const_defined?(part, false)
|
|
127
|
+
|
|
128
|
+
mod = mod.const_get(part, false)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return nil unless mod.is_a?(::String) && !mod.empty?
|
|
132
|
+
|
|
133
|
+
classify_string(mod)
|
|
134
|
+
rescue ::NameError, ::TypeError, ::LoadError
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
private_class_method :inspect_runtime_string
|
|
138
|
+
|
|
139
|
+
# @param value [String] a non-empty string
|
|
140
|
+
# @return [Rigor::Type]
|
|
141
|
+
def self.classify_string(value)
|
|
142
|
+
if Type::Refined.ruby_numeric_literal?(value)
|
|
143
|
+
NUMERIC_STRING
|
|
144
|
+
else
|
|
145
|
+
NON_EMPTY_STRING
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
private_class_method :classify_string
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -13,9 +13,10 @@ module Rigor
|
|
|
13
13
|
module Cache
|
|
14
14
|
# Filesystem-backed cache store. Schema, layout, file format,
|
|
15
15
|
# atomicity, and locking are fixed by [ADR-6](../../../docs/adr/6-cache-persistence-backend.md);
|
|
16
|
-
# callers
|
|
17
|
-
#
|
|
18
|
-
# and
|
|
16
|
+
# callers use `#fetch_or_compute` (producer-keyed),
|
|
17
|
+
# `#fetch_or_validate` (record-and-validate for discovered-dep
|
|
18
|
+
# caches, ADR-45), `#stats`, `#evict!`, and `.disk_inventory`,
|
|
19
|
+
# plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
|
|
19
20
|
#
|
|
20
21
|
# Read failures (missing file, bad magic, format-version mismatch,
|
|
21
22
|
# corrupt SHA-256 trailer, un-inflatable or unmarshal-able payload)
|
|
@@ -119,6 +120,7 @@ module Rigor
|
|
|
119
120
|
# When the root does not exist or has no schema-version
|
|
120
121
|
# marker, `schema_version` is nil and the producer list is
|
|
121
122
|
# empty.
|
|
123
|
+
#
|
|
122
124
|
# The `schema_version.txt` marker content. Covers BOTH
|
|
123
125
|
# invalidation axes: the descriptor schema and the on-disk byte
|
|
124
126
|
# layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "English"
|
|
4
|
+
require "json"
|
|
4
5
|
require "optionparser"
|
|
5
6
|
require "prism"
|
|
6
7
|
|
|
7
8
|
require_relative "../configuration"
|
|
9
|
+
require_relative "options"
|
|
8
10
|
require_relative "../environment"
|
|
9
11
|
require_relative "../scope"
|
|
10
12
|
require_relative "../inference/def_return_typer"
|
|
@@ -37,10 +39,10 @@ module Rigor
|
|
|
37
39
|
# Trailing `#=> …` annotation comment. Matched and stripped
|
|
38
40
|
# before re-annotating so re-running is idempotent — this
|
|
39
41
|
# follows xmpfilter's convention of owning the `#=>` marker,
|
|
40
|
-
# and also absorbs the
|
|
41
|
-
#
|
|
42
|
-
# a string literal (no preceding
|
|
43
|
-
# from matching mid-expression.
|
|
42
|
+
# and also absorbs the older `#=> dump_type: <type>` spelling
|
|
43
|
+
# (idempotency across re-runs). The leading `\s` requirement
|
|
44
|
+
# keeps a `#=>` inside a string literal (no preceding
|
|
45
|
+
# whitespace ambiguity aside) from matching mid-expression.
|
|
44
46
|
ANNOTATION_PATTERN = /\s+#=>(?:\s.*)?\z/
|
|
45
47
|
|
|
46
48
|
# Arguments for highlighting through `bat`: the annotated
|
|
@@ -71,11 +73,15 @@ module Rigor
|
|
|
71
73
|
def parse_options
|
|
72
74
|
# Default: colour a tty, unless `NO_COLOR` opts out. An
|
|
73
75
|
# explicit `--color` / `--no-color` overrides both.
|
|
74
|
-
options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil }
|
|
76
|
+
options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil, format: :text }
|
|
75
77
|
|
|
76
78
|
parser = OptionParser.new do |opts|
|
|
77
79
|
opts.banner = USAGE
|
|
78
|
-
|
|
80
|
+
Options.add_config(opts, options)
|
|
81
|
+
opts.on("--format=FORMAT", %w[text json],
|
|
82
|
+
"Output format: text (default) or json (a { line => type } map)") do |value|
|
|
83
|
+
options[:format] = value.to_sym
|
|
84
|
+
end
|
|
79
85
|
opts.on("--[no-]color",
|
|
80
86
|
"Force or disable ANSI colour (default: auto-detect a tty; honours NO_COLOR)") do |value|
|
|
81
87
|
options[:color] = value
|
|
@@ -122,10 +128,25 @@ module Rigor
|
|
|
122
128
|
)
|
|
123
129
|
line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
case options.fetch(:format)
|
|
132
|
+
when :json
|
|
133
|
+
emit_json(line_types)
|
|
134
|
+
else
|
|
135
|
+
@out.puts(render(annotate(source, line_types), color: options.fetch(:color), bat: options.fetch(:bat)))
|
|
136
|
+
end
|
|
126
137
|
0
|
|
127
138
|
end
|
|
128
139
|
|
|
140
|
+
# `--format json` — emit the { line_number => type } map directly, the
|
|
141
|
+
# same data the text renderer consumes, so clients (the playground,
|
|
142
|
+
# editors) get structured annotations without reparsing the `#=> <type>`
|
|
143
|
+
# comment grammar. Values are the short type descriptions the text form
|
|
144
|
+
# also shows; keys are 1-based line numbers as strings (JSON object keys).
|
|
145
|
+
def emit_json(line_types)
|
|
146
|
+
annotations = line_types.keys.sort.to_h { |line| [line.to_s, line_types[line].describe(:short)] }
|
|
147
|
+
@out.puts(JSON.generate({ "annotations" => annotations }))
|
|
148
|
+
end
|
|
149
|
+
|
|
129
150
|
def base_scope(configuration)
|
|
130
151
|
Scope.empty(
|
|
131
152
|
environment: Environment.for_project(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
|
|
5
5
|
require_relative "../analysis/baseline"
|
|
6
|
+
require_relative "options"
|
|
6
7
|
require_relative "../analysis/runner"
|
|
7
8
|
require_relative "../cache/store"
|
|
8
9
|
require_relative "../configuration"
|
|
@@ -106,7 +107,7 @@ module Rigor
|
|
|
106
107
|
}
|
|
107
108
|
parser = OptionParser.new do |opts|
|
|
108
109
|
opts.banner = "Usage: rigor baseline #{subcommand} [options]"
|
|
109
|
-
|
|
110
|
+
Options.add_config(opts, options)
|
|
110
111
|
opts.on("--output=PATH", "Write baseline to PATH (default: #{DEFAULT_BASELINE_PATH})") do |v|
|
|
111
112
|
options[:output] = v
|
|
112
113
|
end
|
|
@@ -270,7 +271,7 @@ module Rigor
|
|
|
270
271
|
}
|
|
271
272
|
parser = OptionParser.new do |opts|
|
|
272
273
|
opts.banner = "Usage: rigor baseline drift [options]"
|
|
273
|
-
|
|
274
|
+
Options.add_config(opts, options)
|
|
274
275
|
opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
|
|
275
276
|
options[:baseline] = v
|
|
276
277
|
end
|
|
@@ -344,7 +345,7 @@ module Rigor
|
|
|
344
345
|
}
|
|
345
346
|
parser = OptionParser.new do |opts|
|
|
346
347
|
opts.banner = "Usage: rigor baseline prune [options]"
|
|
347
|
-
|
|
348
|
+
Options.add_config(opts, options)
|
|
348
349
|
opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
|
|
349
350
|
options[:baseline] = v
|
|
350
351
|
end
|
|
@@ -6,6 +6,8 @@ require "optionparser"
|
|
|
6
6
|
|
|
7
7
|
require_relative "../configuration"
|
|
8
8
|
require_relative "../analysis/result"
|
|
9
|
+
require_relative "../analysis/rule_catalog"
|
|
10
|
+
require_relative "coverage_scan"
|
|
9
11
|
require_relative "command"
|
|
10
12
|
require_relative "options"
|
|
11
13
|
require_relative "diagnostic_formats"
|
|
@@ -35,6 +37,7 @@ module Rigor
|
|
|
35
37
|
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
36
38
|
|
|
37
39
|
configuration = load_check_configuration(options)
|
|
40
|
+
configuration = apply_bleeding_edge_override(configuration, options)
|
|
38
41
|
cache_root = configuration.cache_path
|
|
39
42
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
40
43
|
|
|
@@ -48,7 +51,8 @@ module Rigor
|
|
|
48
51
|
raw_result = runner.run(@argv.empty? ? configuration.paths : @argv)
|
|
49
52
|
result = apply_baseline_filter(raw_result, configuration, options)
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
coverage = compute_coverage(runner, configuration, options)
|
|
55
|
+
write_result(result, options.fetch(:format), coverage: coverage)
|
|
52
56
|
emit_ci_detected_output(result, options)
|
|
53
57
|
write_run_stats(result.stats) if result.stats
|
|
54
58
|
write_trace_appendices
|
|
@@ -276,29 +280,18 @@ module Rigor
|
|
|
276
280
|
|
|
277
281
|
def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
278
282
|
options = {
|
|
279
|
-
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
280
|
-
# `.rigor.dist.yml`); an explicit `--config=PATH` overrides.
|
|
281
283
|
config: nil,
|
|
282
284
|
format: "text",
|
|
283
285
|
explain: false,
|
|
284
286
|
cache_stats: false,
|
|
285
287
|
clear_cache: false,
|
|
286
288
|
no_cache: false,
|
|
287
|
-
# Run-stats summary (target files, RBS class universe
|
|
288
|
-
# breakdown, wall time, peak RSS) is on by default
|
|
289
|
-
# because collection is ~free (single syscall for RSS,
|
|
290
|
-
# one walk of `class_decl_paths` for the breakdown).
|
|
291
|
-
# `--no-stats` suppresses it for callers that want a
|
|
292
|
-
# diagnostic-only output stream.
|
|
293
289
|
stats: true,
|
|
294
290
|
# ADR-15 Phase 4c — when nil, falls back to
|
|
295
291
|
# `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
|
|
296
292
|
# `parallel.workers:` then 0 (sequential). See
|
|
297
293
|
# `resolve_workers` for the precedence chain.
|
|
298
294
|
workers: nil,
|
|
299
|
-
# Editor mode (`docs/design/20260516-editor-mode.md`).
|
|
300
|
-
# Both must appear together; the runner uses the pair
|
|
301
|
-
# to bind an in-flight buffer file to its logical path.
|
|
302
295
|
tmp_file: nil,
|
|
303
296
|
instead_of: nil,
|
|
304
297
|
# ADR-22 — baseline filter. `:unset` means "fall through
|
|
@@ -335,17 +328,34 @@ module Rigor
|
|
|
335
328
|
# the human output; for GitLab / reviewdog-routed CIs, print a
|
|
336
329
|
# one-line hint. On by default; `--no-ci-detect` (or
|
|
337
330
|
# `RIGOR_CI_DETECT=0`) disables it.
|
|
338
|
-
ci_detect: true
|
|
331
|
+
ci_detect: true,
|
|
332
|
+
# ADR-50 § WD2 — the `--bleeding-edge[=ids]` / `--no-bleeding-edge`
|
|
333
|
+
# CLI mirror of the `bleeding_edge:` config key. `:unset` means "no
|
|
334
|
+
# flag — use the configured selection"; `true` adopts the whole
|
|
335
|
+
# overlay, `false` adopts none, and an Array of ids adopts only
|
|
336
|
+
# those (see `apply_bleeding_edge_override`).
|
|
337
|
+
bleeding_edge: :unset,
|
|
338
|
+
# Type-precision coverage block. Off by default — it is a
|
|
339
|
+
# second precision pass over the analyzed files (the same scan
|
|
340
|
+
# `rigor coverage` runs), so it is opt-in to keep the default
|
|
341
|
+
# check path's cost unchanged. When set, `--format json` gains
|
|
342
|
+
# a `coverage` object (scan_files + precision tiers) and the
|
|
343
|
+
# text output prints a one-line coverage summary.
|
|
344
|
+
coverage: false
|
|
339
345
|
}
|
|
340
346
|
parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
341
347
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
342
|
-
|
|
348
|
+
Options.add_config(opts, options)
|
|
343
349
|
opts.on("--format=FORMAT",
|
|
344
350
|
"Output format: text, json, sarif, github, gitlab, checkstyle, junit, teamcity") do |value|
|
|
345
351
|
options[:format] = value
|
|
346
352
|
end
|
|
347
353
|
opts.on("--explain", "Surface fail-soft fallback events as :info diagnostics") { options[:explain] = true }
|
|
348
354
|
opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
|
|
355
|
+
opts.on("--coverage",
|
|
356
|
+
"Add a type-precision coverage block (an extra precision pass over the analyzed files)") do
|
|
357
|
+
options[:coverage] = true
|
|
358
|
+
end
|
|
349
359
|
opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
|
|
350
360
|
opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
|
|
351
361
|
opts.on("--[no-]stats",
|
|
@@ -385,6 +395,18 @@ module Rigor
|
|
|
385
395
|
"ADR-51: do not auto-emit CI-native output when a CI environment is detected") do
|
|
386
396
|
options[:ci_detect] = false
|
|
387
397
|
end
|
|
398
|
+
# ADR-50 § WD2 — `=[LIST]` (not ` [LIST]`) so a bare `--bleeding-edge`
|
|
399
|
+
# never swallows a following positional path: `rigor check
|
|
400
|
+
# --bleeding-edge lib` adopts the whole overlay and checks `lib`.
|
|
401
|
+
opts.on("--bleeding-edge=[LIST]",
|
|
402
|
+
"ADR-50: adopt the bleeding-edge overlay for this run " \
|
|
403
|
+
"(all features, or a comma-separated feature-id list)") do |value|
|
|
404
|
+
options[:bleeding_edge] = value.nil? || value.split(",").map(&:strip).reject(&:empty?)
|
|
405
|
+
end
|
|
406
|
+
opts.on("--no-bleeding-edge",
|
|
407
|
+
"ADR-50: ignore any configured bleeding_edge: selection for this run") do
|
|
408
|
+
options[:bleeding_edge] = false
|
|
409
|
+
end
|
|
388
410
|
end
|
|
389
411
|
parser.parse!(@argv)
|
|
390
412
|
options
|
|
@@ -410,6 +432,20 @@ module Rigor
|
|
|
410
432
|
Configuration.new(Configuration::DEFAULTS.merge(data))
|
|
411
433
|
end
|
|
412
434
|
|
|
435
|
+
# ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge`
|
|
436
|
+
# CLI selection over the configured `bleeding_edge:` value, mirroring the
|
|
437
|
+
# CLI-over-config precedence `--workers` and `--no-cache` follow. `:unset`
|
|
438
|
+
# (no flag) leaves the loaded configuration untouched; any other value is
|
|
439
|
+
# normalised by {Configuration#with_bleeding_edge}, so the two
|
|
440
|
+
# `SeverityProfile.resolve` sites (and the worker path, which receives the
|
|
441
|
+
# whole frozen Configuration) see the run's selection.
|
|
442
|
+
def apply_bleeding_edge_override(configuration, options)
|
|
443
|
+
selection = options.fetch(:bleeding_edge)
|
|
444
|
+
return configuration if selection == :unset
|
|
445
|
+
|
|
446
|
+
configuration.with_bleeding_edge(selection)
|
|
447
|
+
end
|
|
448
|
+
|
|
413
449
|
def inject_treat_all_as_inline_rbs(entries)
|
|
414
450
|
filtered = entries.reject { |entry| rigor_rbs_inline_entry?(entry) }
|
|
415
451
|
filtered + [{
|
|
@@ -635,12 +671,15 @@ module Rigor
|
|
|
635
671
|
format("%.1f MiB", bytes / (1024.0 * 1024.0))
|
|
636
672
|
end
|
|
637
673
|
|
|
638
|
-
def write_result(result, format)
|
|
674
|
+
def write_result(result, format, coverage: nil)
|
|
639
675
|
case format
|
|
640
676
|
when "json"
|
|
641
|
-
|
|
677
|
+
payload = enrich_json(result.to_h)
|
|
678
|
+
payload["coverage"] = coverage_payload(coverage) if coverage
|
|
679
|
+
@out.puts(JSON.pretty_generate(payload))
|
|
642
680
|
when "text"
|
|
643
681
|
write_text_result(result)
|
|
682
|
+
write_coverage_summary(coverage) if coverage
|
|
644
683
|
when ->(fmt) { CLI::DiagnosticFormats.supports?(fmt) }
|
|
645
684
|
# ADR-51 — CI-native renderings (SARIF / GitHub Actions commands /
|
|
646
685
|
# GitLab Code Quality). The `github` form is empty when there are no
|
|
@@ -652,6 +691,66 @@ module Rigor
|
|
|
652
691
|
end
|
|
653
692
|
end
|
|
654
693
|
|
|
694
|
+
# Runs the type-precision scan (`--coverage`) over the same file set
|
|
695
|
+
# the check analyzed and returns a `CoverageReport`, or nil when the
|
|
696
|
+
# flag is off. It is a second pass — the same scan `rigor coverage`
|
|
697
|
+
# runs, reused via {CoverageScan} — so it is opt-in to keep the
|
|
698
|
+
# default check path's cost unchanged.
|
|
699
|
+
def compute_coverage(runner, configuration, options)
|
|
700
|
+
return nil unless options.fetch(:coverage)
|
|
701
|
+
|
|
702
|
+
files = @argv.empty? ? runner.analysis_file_set : runner.analysis_file_set(@argv)
|
|
703
|
+
CoverageScan.precision_report(files: files, configuration: configuration)
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# The `coverage` block embedded in `--format json`. Mirrors the
|
|
707
|
+
# `summary` of `rigor coverage --format json` (the same vocabulary —
|
|
708
|
+
# `precise_ratio`, not a separate `typed_ratio`) plus `scan_files`,
|
|
709
|
+
# so a consumer reads one stream to learn both what fired and how
|
|
710
|
+
# much of the analyzed surface Rigor could type.
|
|
711
|
+
def coverage_payload(report)
|
|
712
|
+
{
|
|
713
|
+
"scan_files" => report.files.size - report.parse_errors.size,
|
|
714
|
+
"parse_errors" => report.parse_errors.size,
|
|
715
|
+
"expressions_typed" => report.grand_total,
|
|
716
|
+
"precise_count" => report.precise_count,
|
|
717
|
+
"precise_ratio" => report.precision_ratio.round(4),
|
|
718
|
+
"dynamic_opaque_count" => report.opaque_count,
|
|
719
|
+
"dynamic_opaque_ratio" => report.opaque_ratio.round(4)
|
|
720
|
+
}
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def write_coverage_summary(report)
|
|
724
|
+
files = report.files.size - report.parse_errors.size
|
|
725
|
+
pct = (report.precision_ratio * 100).round(1)
|
|
726
|
+
@out.puts("Type coverage: #{files} file(s), #{pct}% precise " \
|
|
727
|
+
"(#{report.precise_count}/#{report.grand_total} expressions). " \
|
|
728
|
+
"Run `rigor coverage` for the full per-file / per-tier breakdown.")
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# Adds the per-rule `evidence_tier` and `documentation_url` fields
|
|
732
|
+
# to each diagnostic in the `--format json` payload. Both are pure
|
|
733
|
+
# functions of the rule id (the rule catalogue, ADR-61 / the
|
|
734
|
+
# 2026-06-15 feedback §4 + §5.1), so they enrich the presentation
|
|
735
|
+
# layer here rather than threading through every diagnostic
|
|
736
|
+
# construction site. Only built-in rules carry catalogue metadata;
|
|
737
|
+
# plugin / `rbs_extended` / parse-error diagnostics are left
|
|
738
|
+
# untouched (they host their own documentation and confidence).
|
|
739
|
+
def enrich_json(payload)
|
|
740
|
+
Array(payload["diagnostics"]).each do |diag|
|
|
741
|
+
next unless diag["source_family"] == Analysis::Diagnostic::DEFAULT_SOURCE_FAMILY.to_s
|
|
742
|
+
|
|
743
|
+
rule = diag["rule"]
|
|
744
|
+
next unless rule
|
|
745
|
+
|
|
746
|
+
tier = Analysis::RuleCatalog.evidence_tier(rule)
|
|
747
|
+
diag["evidence_tier"] = tier.to_s if tier
|
|
748
|
+
url = Analysis::RuleCatalog.documentation_url(rule)
|
|
749
|
+
diag["documentation_url"] = url if url
|
|
750
|
+
end
|
|
751
|
+
payload
|
|
752
|
+
end
|
|
753
|
+
|
|
655
754
|
# ADR-51 WD7 — CI auto-detection. Only augments the default human
|
|
656
755
|
# (`text`) output: an explicit `--format` means the caller is in control
|
|
657
756
|
# and is left untouched. For a first-class stdout-native CI (GitHub
|