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
|
@@ -13,8 +13,8 @@ module Rigor
|
|
|
13
13
|
# ([`Rigor::Cache::Store`](store.rb), v0.0.8 slice 2) consumes
|
|
14
14
|
# descriptors but does not extend them.
|
|
15
15
|
#
|
|
16
|
-
# The descriptor has
|
|
17
|
-
# `configs`); every slot is an array of typed entries; an empty
|
|
16
|
+
# The descriptor has six slots (`files`, `gems`, `plugins`,
|
|
17
|
+
# `configs`, `dependencies`, `globs`); every slot is an array of typed entries; an empty
|
|
18
18
|
# array means "no dependency in this slot". Composition unions
|
|
19
19
|
# by key per slot; conflicts on the comparison fields raise
|
|
20
20
|
# {Conflict}.
|
|
@@ -32,7 +32,12 @@ module Rigor
|
|
|
32
32
|
# references but never declares, so the marshalled RBS env
|
|
33
33
|
# cached by an older Rigor (which would leave those signatures
|
|
34
34
|
# inert) MUST be rebuilt for the synthesis to take effect.
|
|
35
|
-
|
|
35
|
+
# v4: ADR-60 WD3 added the `globs` slot ({GlobEntry}) for the
|
|
36
|
+
# record-and-validate plugin-producer cache; the new slot
|
|
37
|
+
# changes `#to_canonical_hash` (and is Marshal-dumped inside
|
|
38
|
+
# `fetch_or_validate` entry pairs), so entries written by an
|
|
39
|
+
# older Rigor must read as misses.
|
|
40
|
+
SCHEMA_VERSION = 4
|
|
36
41
|
|
|
37
42
|
# Per-slot entry value objects. Constructors validate enums /
|
|
38
43
|
# required fields and freeze the resulting struct so no caller
|
|
@@ -160,6 +165,62 @@ module Rigor
|
|
|
160
165
|
end
|
|
161
166
|
end
|
|
162
167
|
|
|
168
|
+
# ADR-60 WD3 — one glob's-worth of watched files, digested as a
|
|
169
|
+
# single value so the entry covers content change, addition,
|
|
170
|
+
# AND removal in one row: the digest is the SHA-256 over the
|
|
171
|
+
# sorted `"<path>\0<sha256-of-content>\n"` rows of every file
|
|
172
|
+
# matching `File.join(root, pattern)`. A new file adds a row, a
|
|
173
|
+
# deleted file drops one, an edit changes one — all three move
|
|
174
|
+
# the digest. {Descriptor#fresh?} re-runs the same computation
|
|
175
|
+
# and compares.
|
|
176
|
+
class GlobEntry
|
|
177
|
+
include Rigor::ValueSemantics
|
|
178
|
+
|
|
179
|
+
attr_reader :root, :pattern, :value
|
|
180
|
+
|
|
181
|
+
value_fields :root, :pattern, :value
|
|
182
|
+
|
|
183
|
+
def initialize(root:, pattern:, value:)
|
|
184
|
+
@root = root.to_s.dup.freeze
|
|
185
|
+
@pattern = pattern.to_s.dup.freeze
|
|
186
|
+
@value = value.to_s.dup.freeze
|
|
187
|
+
freeze
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Builds the entry for the glob's CURRENT filesystem state.
|
|
191
|
+
def self.compute(root:, pattern:)
|
|
192
|
+
new(root: root, pattern: pattern, value: digest_for(root: root, pattern: pattern))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The digest the entry's `value` carries. Per-file read
|
|
196
|
+
# failures (a file vanishing between the glob and the
|
|
197
|
+
# digest) are treated as the file being absent — same
|
|
198
|
+
# race posture as {Descriptor#file_entry_fresh?}.
|
|
199
|
+
def self.digest_for(root:, pattern:)
|
|
200
|
+
# Dir.glob returns sorted entries by default (sort: true),
|
|
201
|
+
# so the row order — and therefore the digest — is stable.
|
|
202
|
+
rows = Dir.glob(File.join(root, pattern)).filter_map do |path|
|
|
203
|
+
next nil unless File.file?(path)
|
|
204
|
+
|
|
205
|
+
"#{path}\0#{Digest::SHA256.file(path).hexdigest}\n"
|
|
206
|
+
rescue StandardError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
Digest::SHA256.hexdigest(rows.join)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Composition key — {.compose} unions per (root, pattern)
|
|
213
|
+
# slot; two contributions for the same slot must agree on
|
|
214
|
+
# the digest or {Conflict} is raised.
|
|
215
|
+
def slot_key
|
|
216
|
+
"#{root}\0#{pattern}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def to_h
|
|
220
|
+
{ "root" => root, "pattern" => pattern, "value" => value }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
163
224
|
# Raised when {.compose} encounters incompatible entries
|
|
164
225
|
# under the same key (file digest mismatch, gem-locked
|
|
165
226
|
# disagreement, …). Callers handle the exception by
|
|
@@ -167,14 +228,15 @@ module Rigor
|
|
|
167
228
|
# contribution silently.
|
|
168
229
|
class Conflict < StandardError; end
|
|
169
230
|
|
|
170
|
-
attr_reader :files, :gems, :plugins, :configs, :dependencies
|
|
231
|
+
attr_reader :files, :gems, :plugins, :configs, :dependencies, :globs
|
|
171
232
|
|
|
172
|
-
def initialize(files: [], gems: [], plugins: [], configs: [], dependencies: [])
|
|
233
|
+
def initialize(files: [], gems: [], plugins: [], configs: [], dependencies: [], globs: [])
|
|
173
234
|
@files = files.dup.freeze
|
|
174
235
|
@gems = gems.dup.freeze
|
|
175
236
|
@plugins = plugins.dup.freeze
|
|
176
237
|
@configs = configs.dup.freeze
|
|
177
238
|
@dependencies = dependencies.dup.freeze
|
|
239
|
+
@globs = globs.dup.freeze
|
|
178
240
|
freeze
|
|
179
241
|
end
|
|
180
242
|
|
|
@@ -185,11 +247,15 @@ module Rigor
|
|
|
185
247
|
# `files` are checked — non-file inputs (config / gems / version)
|
|
186
248
|
# belong in the cache *key*, not the validated dependency set — so
|
|
187
249
|
# a descriptor carrying any non-file slot is never considered fresh
|
|
188
|
-
# (it was built wrong for this use).
|
|
250
|
+
# (it was built wrong for this use). ADR-60 WD3 adds `globs`
|
|
251
|
+
# alongside `files` as a re-validatable slot: a {GlobEntry} is
|
|
252
|
+
# fresh when re-globbing + re-digesting reproduces its recorded
|
|
253
|
+
# value.
|
|
189
254
|
def fresh?
|
|
190
255
|
return false unless gems.empty? && plugins.empty? && configs.empty? && dependencies.empty?
|
|
191
256
|
|
|
192
|
-
files.all? { |entry| file_entry_fresh?(entry) }
|
|
257
|
+
files.all? { |entry| file_entry_fresh?(entry) } &&
|
|
258
|
+
globs.all? { |entry| glob_entry_fresh?(entry) }
|
|
193
259
|
end
|
|
194
260
|
|
|
195
261
|
# File-comparator strictness ordering. `:digest` is strictest
|
|
@@ -212,7 +278,9 @@ module Rigor
|
|
|
212
278
|
plugins = compose_by_key(descriptors.flat_map(&:plugins), :id)
|
|
213
279
|
configs = compose_by_key(descriptors.flat_map(&:configs), :key)
|
|
214
280
|
dependencies = compose_by_key(descriptors.flat_map(&:dependencies), :gem_name)
|
|
215
|
-
|
|
281
|
+
globs = compose_by_key(descriptors.flat_map(&:globs), :slot_key)
|
|
282
|
+
new(files: files, gems: gems, plugins: plugins, configs: configs,
|
|
283
|
+
dependencies: dependencies, globs: globs)
|
|
216
284
|
end
|
|
217
285
|
|
|
218
286
|
# @param producer_id [String]
|
|
@@ -241,6 +309,7 @@ module Rigor
|
|
|
241
309
|
"dependencies" => sort_entries(dependencies, "gem_name").map(&:to_h),
|
|
242
310
|
"files" => sort_entries(files, "path").map(&:to_h),
|
|
243
311
|
"gems" => sort_entries(gems, "name").map(&:to_h),
|
|
312
|
+
"globs" => globs.sort_by { |e| [e.root, e.pattern] }.map(&:to_h),
|
|
244
313
|
"plugins" => sort_entries(plugins, "id").map(&:to_h)
|
|
245
314
|
}
|
|
246
315
|
end
|
|
@@ -291,6 +360,15 @@ module Rigor
|
|
|
291
360
|
false
|
|
292
361
|
end
|
|
293
362
|
|
|
363
|
+
# ADR-60 WD3 — re-runs the entry's glob + digest and compares
|
|
364
|
+
# against the recorded value. Any failure reads as stale
|
|
365
|
+
# (recompute), never a crash.
|
|
366
|
+
def glob_entry_fresh?(entry)
|
|
367
|
+
GlobEntry.digest_for(root: entry.root, pattern: entry.pattern) == entry.value
|
|
368
|
+
rescue StandardError
|
|
369
|
+
false
|
|
370
|
+
end
|
|
371
|
+
|
|
294
372
|
def sort_entries(entries, key)
|
|
295
373
|
entries.sort_by { |e| e.to_h.fetch(key).to_s }
|
|
296
374
|
end
|
|
@@ -29,7 +29,8 @@ module Rigor
|
|
|
29
29
|
|
|
30
30
|
def self.file_entries(loader)
|
|
31
31
|
roots = loader.signature_paths +
|
|
32
|
-
Rigor::Environment::RbsLoader.vendored_gem_sig_paths
|
|
32
|
+
Rigor::Environment::RbsLoader.vendored_gem_sig_paths +
|
|
33
|
+
Rigor::Environment::RbsLoader.core_overlay_sig_paths
|
|
33
34
|
roots.flat_map do |root|
|
|
34
35
|
next [] unless root.directory?
|
|
35
36
|
|
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,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
4
|
+
require "json"
|
|
3
5
|
require "optionparser"
|
|
4
6
|
require "prism"
|
|
5
7
|
|
|
6
8
|
require_relative "../configuration"
|
|
9
|
+
require_relative "options"
|
|
7
10
|
require_relative "../environment"
|
|
8
11
|
require_relative "../scope"
|
|
9
12
|
require_relative "../inference/def_return_typer"
|
|
10
13
|
require_relative "../inference/scope_indexer"
|
|
14
|
+
require_relative "../inference/statement_evaluator"
|
|
11
15
|
require_relative "prism_colorizer"
|
|
12
16
|
require_relative "command"
|
|
13
17
|
|
|
@@ -20,18 +24,33 @@ module Rigor
|
|
|
20
24
|
# (so `1; 2; 3` reports `3`), or, for a line that no statement
|
|
21
25
|
# closes, the widest expression ending there (so the `if nil`
|
|
22
26
|
# header reports its condition). It infers that expression's
|
|
23
|
-
# type and appends a `#=>
|
|
27
|
+
# type and appends a `#=> <type>` comment (the xmpfilter /
|
|
28
|
+
# seeing_is_believing convention).
|
|
24
29
|
#
|
|
25
30
|
# The annotated source is re-parsed with Prism — a sanity gate,
|
|
26
31
|
# since the appended text is always a comment — and printed to
|
|
27
|
-
# stdout
|
|
32
|
+
# stdout. When colour is enabled and `bat`
|
|
33
|
+
# (https://github.com/sharkdp/bat) is on PATH it is used for
|
|
34
|
+
# highlighting; otherwise IRB-style highlighting via
|
|
28
35
|
# {PrismColorizer}.
|
|
29
36
|
class AnnotateCommand < Command
|
|
30
37
|
USAGE = "Usage: rigor annotate [options] FILE"
|
|
31
38
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
39
|
+
# Trailing `#=> …` annotation comment. Matched and stripped
|
|
40
|
+
# before re-annotating so re-running is idempotent — this
|
|
41
|
+
# follows xmpfilter's convention of owning the `#=>` marker,
|
|
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.
|
|
46
|
+
ANNOTATION_PATTERN = /\s+#=>(?:\s.*)?\z/
|
|
47
|
+
|
|
48
|
+
# Arguments for highlighting through `bat`: the annotated
|
|
49
|
+
# text arrives on stdin, so the language must be explicit;
|
|
50
|
+
# `--style=plain` drops the grid/header chrome so the output
|
|
51
|
+
# matches the PrismColorizer fallback line-for-line; paging
|
|
52
|
+
# stays off because the CLI may itself sit in a pipeline.
|
|
53
|
+
BAT_ARGS = %w[--language=ruby --style=plain --paging=never --color=always].freeze
|
|
35
54
|
|
|
36
55
|
# @return [Integer] CLI exit status.
|
|
37
56
|
def run
|
|
@@ -54,15 +73,23 @@ module Rigor
|
|
|
54
73
|
def parse_options
|
|
55
74
|
# Default: colour a tty, unless `NO_COLOR` opts out. An
|
|
56
75
|
# explicit `--color` / `--no-color` overrides both.
|
|
57
|
-
options = { config: nil, color: @out.tty? && !no_color_env
|
|
76
|
+
options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil, format: :text }
|
|
58
77
|
|
|
59
78
|
parser = OptionParser.new do |opts|
|
|
60
79
|
opts.banner = USAGE
|
|
61
|
-
|
|
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
|
|
62
85
|
opts.on("--[no-]color",
|
|
63
86
|
"Force or disable ANSI colour (default: auto-detect a tty; honours NO_COLOR)") do |value|
|
|
64
87
|
options[:color] = value
|
|
65
88
|
end
|
|
89
|
+
opts.on("--[no-]bat",
|
|
90
|
+
"Force or disable highlighting through bat (default: when colour is on and bat is found)") do |value|
|
|
91
|
+
options[:bat] = value
|
|
92
|
+
end
|
|
66
93
|
end
|
|
67
94
|
parser.parse!(@argv)
|
|
68
95
|
|
|
@@ -91,15 +118,35 @@ module Rigor
|
|
|
91
118
|
parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
|
|
92
119
|
return 1 if parse_errors?(parse_result, file)
|
|
93
120
|
|
|
121
|
+
# `converged_loop_recording` re-records fixpoint-tracked loop
|
|
122
|
+
# bodies from their converged (post-writeback) bindings, so a
|
|
123
|
+
# loop-body line annotates the joined widened type (`Integer`)
|
|
124
|
+
# rather than a stale first-iterations constant (`1 | 2`).
|
|
94
125
|
scope_index = Inference::ScopeIndexer.index(
|
|
95
|
-
parse_result.value, default_scope: base_scope(configuration)
|
|
126
|
+
parse_result.value, default_scope: base_scope(configuration),
|
|
127
|
+
converged_loop_recording: true
|
|
96
128
|
)
|
|
97
129
|
line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
|
|
98
130
|
|
|
99
|
-
|
|
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
|
|
100
137
|
0
|
|
101
138
|
end
|
|
102
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
|
+
|
|
103
150
|
def base_scope(configuration)
|
|
104
151
|
Scope.empty(
|
|
105
152
|
environment: Environment.for_project(
|
|
@@ -118,8 +165,8 @@ module Rigor
|
|
|
118
165
|
true
|
|
119
166
|
end
|
|
120
167
|
|
|
121
|
-
# Appends ` #=>
|
|
122
|
-
#
|
|
168
|
+
# Appends ` #=> <type>` to every line a type was inferred
|
|
169
|
+
# for, aligning the comment column.
|
|
123
170
|
def annotate(source, line_types)
|
|
124
171
|
lines = source.lines
|
|
125
172
|
column = annotation_column(lines, line_types)
|
|
@@ -130,7 +177,7 @@ module Rigor
|
|
|
130
177
|
code = line.chomp.sub(ANNOTATION_PATTERN, "")
|
|
131
178
|
next "#{code}#{eol}" if type.nil?
|
|
132
179
|
|
|
133
|
-
"#{code.ljust(column)} #=>
|
|
180
|
+
"#{code.ljust(column)} #=> #{type.describe(:short)}#{eol}"
|
|
134
181
|
end.join
|
|
135
182
|
end
|
|
136
183
|
|
|
@@ -143,11 +190,46 @@ module Rigor
|
|
|
143
190
|
widths.max || 0
|
|
144
191
|
end
|
|
145
192
|
|
|
146
|
-
def render(annotated, color:)
|
|
193
|
+
def render(annotated, color:, bat: nil)
|
|
147
194
|
return annotated unless color
|
|
148
195
|
return annotated unless Prism.parse(annotated).success?
|
|
149
196
|
|
|
150
|
-
|
|
197
|
+
rendered = render_with_bat(annotated, forced: bat) unless bat == false
|
|
198
|
+
rendered || PrismColorizer.colorize(annotated)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Pipes the annotated source through `bat` and returns its
|
|
202
|
+
# highlighted output, or nil when bat is unavailable or
|
|
203
|
+
# fails (broken install, killed mid-write) — the caller
|
|
204
|
+
# falls back to {PrismColorizer}. An explicit `--bat` with
|
|
205
|
+
# no bat on PATH warns instead of failing silently.
|
|
206
|
+
def render_with_bat(annotated, forced: nil)
|
|
207
|
+
executable = bat_executable
|
|
208
|
+
if executable.nil?
|
|
209
|
+
@err.puts("annotate: --bat requested but no `bat` executable found on PATH") if forced
|
|
210
|
+
return nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
output = IO.popen([executable, *BAT_ARGS], "r+") do |io|
|
|
214
|
+
io.write(annotated)
|
|
215
|
+
io.close_write
|
|
216
|
+
io.read
|
|
217
|
+
end
|
|
218
|
+
return nil unless $CHILD_STATUS&.success?
|
|
219
|
+
|
|
220
|
+
output.empty? ? nil : output
|
|
221
|
+
rescue SystemCallError, IOError
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def bat_executable
|
|
226
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
227
|
+
next if dir.empty?
|
|
228
|
+
|
|
229
|
+
candidate = File.join(dir, "bat")
|
|
230
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
231
|
+
end
|
|
232
|
+
nil
|
|
151
233
|
end
|
|
152
234
|
end
|
|
153
235
|
|
|
@@ -203,11 +285,30 @@ module Rigor
|
|
|
203
285
|
widest_per_line(program).each do |line, node|
|
|
204
286
|
next if by_line.key?(line)
|
|
205
287
|
|
|
206
|
-
type = type_of(node)
|
|
288
|
+
type = node.is_a?(Prism::BlockParametersNode) ? block_params_type(node) : type_of(node)
|
|
207
289
|
by_line[line] = type unless type.nil?
|
|
208
290
|
end
|
|
209
291
|
end
|
|
210
292
|
|
|
293
|
+
# A `do |i|` header line's widest node is its BlockParametersNode —
|
|
294
|
+
# not an expression, so evaluating it would only echo the
|
|
295
|
+
# `Dynamic[top]` fallback. Annotate the line with the parameters'
|
|
296
|
+
# inferred bindings instead (the single param's type, or a tuple
|
|
297
|
+
# for multi-param blocks); decline (nil) when any param has no
|
|
298
|
+
# plain name or no recorded binding, leaving the line bare.
|
|
299
|
+
def block_params_type(params_node)
|
|
300
|
+
inner = params_node.parameters
|
|
301
|
+
return nil if inner.nil? || inner.requireds.empty?
|
|
302
|
+
|
|
303
|
+
scope = @scope_index[params_node]
|
|
304
|
+
types = inner.requireds.map do |param|
|
|
305
|
+
return nil unless param.respond_to?(:name)
|
|
306
|
+
|
|
307
|
+
scope.local(param.name) or return nil
|
|
308
|
+
end
|
|
309
|
+
types.size == 1 ? types.first : Type::Combinator.tuple_of(*types)
|
|
310
|
+
end
|
|
311
|
+
|
|
211
312
|
def widest_per_line(program)
|
|
212
313
|
widest = {}
|
|
213
314
|
walk(program) do |node|
|
|
@@ -231,8 +332,13 @@ module Rigor
|
|
|
231
332
|
node.compact_child_nodes.each { |child| walk(child, &block) }
|
|
232
333
|
end
|
|
233
334
|
|
|
335
|
+
# Types the node through the flow evaluator (not the bare
|
|
336
|
+
# expression typer) under its recorded entry scope, so flow-only
|
|
337
|
+
# forms type as the engine sees them — `i += 1` dispatches `+` on
|
|
338
|
+
# `i`'s binding (`Integer`, post-fixpoint) instead of echoing the
|
|
339
|
+
# RHS literal's `1`.
|
|
234
340
|
def type_of(node)
|
|
235
|
-
@scope_index[node].
|
|
341
|
+
Inference::StatementEvaluator.new(scope: @scope_index[node]).evaluate(node).first
|
|
236
342
|
rescue StandardError
|
|
237
343
|
nil
|
|
238
344
|
end
|
|
@@ -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
|