rigortype 0.1.18 → 0.1.19
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 +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +169 -23
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +266 -63
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +2 -1
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -1
- data/lib/rigor/analysis/runner.rb +58 -21
- data/lib/rigor/analysis/worker_session.rb +21 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +3 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +57 -1
- data/lib/rigor/environment/rbs_loader.rb +25 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1052 -43
- 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/constant_folding.rb +54 -14
- 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 +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +72 -1
- 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 +142 -0
- data/lib/rigor/inference/narrowing.rb +270 -37
- data/lib/rigor/inference/scope_indexer.rb +696 -25
- data/lib/rigor/inference/statement_evaluator.rb +963 -16
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/base.rb +235 -79
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +12 -11
- data/lib/rigor/scope/discovery_index.rb +2 -0
- data/lib/rigor/scope.rb +132 -6
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +20 -19
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +10 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -13
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- 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.rb +25 -0
- 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 +10 -1
- data/sig/rigor/type.rbs +1 -0
- 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 +7 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -228,7 +228,7 @@ module Rigor
|
|
|
228
228
|
return unless call.is_a?(Prism::CallNode) && call.receiver.nil? && call.name == template.variant_method
|
|
229
229
|
|
|
230
230
|
args = call.arguments&.arguments || []
|
|
231
|
-
variant_const = const_name_string(args[template.
|
|
231
|
+
variant_const = const_name_string(args[template.symbol_arg_position])
|
|
232
232
|
return if variant_const.nil?
|
|
233
233
|
|
|
234
234
|
yield variant_const, args[template.inner_arg_position]
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -74,23 +74,59 @@ module Rigor
|
|
|
74
74
|
# argument; the same params Hash mixes into the cache
|
|
75
75
|
# key per `Cache::Descriptor#cache_key_for`.
|
|
76
76
|
#
|
|
77
|
-
# `serialize:` / `deserialize:`
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
77
|
+
# `serialize:` / `deserialize:` apply to the producer's
|
|
78
|
+
# return VALUE (the cache layer wraps them around the
|
|
79
|
+
# record-and-validate entry pair itself). Default
|
|
80
|
+
# round-trip is `Marshal.dump` / `Marshal.load` per the
|
|
81
|
+
# v0.0.9 callable surface; producers whose return values
|
|
82
|
+
# are not Marshal-clean must supply their own pair.
|
|
83
|
+
#
|
|
84
|
+
# `watch:` (ADR-60 WD3) declares the glob coverage of a
|
|
85
|
+
# discovery-style producer — the files whose addition /
|
|
86
|
+
# removal / edit must invalidate the cached value even
|
|
87
|
+
# when the producer block never read them individually
|
|
88
|
+
# (e.g. it globbed a directory itself). It is either
|
|
89
|
+
#
|
|
90
|
+
# - a static Array of `[roots, pattern, ...]` tuples
|
|
91
|
+
# (`roots` a String or Array of Strings; one or more
|
|
92
|
+
# glob-pattern suffixes per tuple — the same shape
|
|
93
|
+
# {#glob_descriptor} takes), or
|
|
94
|
+
# - a Proc, run through `instance_exec` on the plugin
|
|
95
|
+
# instance at `cache_for` invocation time (NEVER at
|
|
96
|
+
# class-definition time — search roots are typically
|
|
97
|
+
# computed in `#init` from config), returning the same
|
|
98
|
+
# tuple Array.
|
|
99
|
+
#
|
|
100
|
+
# The evaluated tuples become {Cache::Descriptor::GlobEntry}
|
|
101
|
+
# rows in the dependency descriptor recorded after the
|
|
102
|
+
# block runs; `Descriptor#fresh?` re-globs + re-digests on
|
|
103
|
+
# the next run.
|
|
82
104
|
#
|
|
83
105
|
# Producer ids are auto-prefixed `plugin.<manifest.id>.`
|
|
84
106
|
# at the cache layer (slice 6-C) so plugin-side ids cannot
|
|
85
107
|
# collide with built-in producers.
|
|
86
|
-
def producer(id, serialize: nil, deserialize: nil, &block)
|
|
108
|
+
def producer(id, watch: nil, serialize: nil, deserialize: nil, &block)
|
|
87
109
|
raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?
|
|
88
110
|
|
|
111
|
+
validate_producer_watch!(watch)
|
|
89
112
|
@producers ||= {}
|
|
90
|
-
@producers[id.to_sym] = {
|
|
113
|
+
@producers[id.to_sym] = {
|
|
114
|
+
block: block, watch: watch, serialize: serialize, deserialize: deserialize
|
|
115
|
+
}.freeze
|
|
91
116
|
id.to_sym
|
|
92
117
|
end
|
|
93
118
|
|
|
119
|
+
# ADR-60 WD3 — `watch:` is nil (no glob coverage), a static
|
|
120
|
+
# tuple Array, or a Proc evaluated per `cache_for` call.
|
|
121
|
+
def validate_producer_watch!(watch)
|
|
122
|
+
return if watch.nil? || watch.is_a?(Array) || watch.respond_to?(:call)
|
|
123
|
+
|
|
124
|
+
raise ArgumentError,
|
|
125
|
+
"Plugin::Base.producer watch: must be nil, an Array of [roots, pattern, ...] tuples, " \
|
|
126
|
+
"or a Proc returning one, got #{watch.inspect}"
|
|
127
|
+
end
|
|
128
|
+
private :validate_producer_watch!
|
|
129
|
+
|
|
94
130
|
# Frozen snapshot of the producer table. Inherited
|
|
95
131
|
# producers from a superclass are intentionally NOT
|
|
96
132
|
# surfaced — Plugin::Base subclasses do not chain
|
|
@@ -445,6 +481,13 @@ module Rigor
|
|
|
445
481
|
# memo-on-first-dispatch is a Hash-content mutation, sound even on
|
|
446
482
|
# a self-freezing plugin.
|
|
447
483
|
@dynamic_return_runtime_cache = {}
|
|
484
|
+
# ADR-60 WD4 — nil-inclusive memo tables for the authoring
|
|
485
|
+
# helpers ({#read_fact} / {#producer_value} / {#producer_error}).
|
|
486
|
+
# Allocated here, before any subclass `initialize` self-freeze,
|
|
487
|
+
# for the same reason: a populate is a Hash-content mutation.
|
|
488
|
+
@fact_cache = {}
|
|
489
|
+
@producer_value_cache = {}
|
|
490
|
+
@producer_errors = {}
|
|
448
491
|
end
|
|
449
492
|
|
|
450
493
|
# Override in subclasses to wire any state the plugin needs
|
|
@@ -625,6 +668,69 @@ module Rigor
|
|
|
625
668
|
)
|
|
626
669
|
end
|
|
627
670
|
|
|
671
|
+
# ADR-60 WD4 — maps a plugin's own violation objects to
|
|
672
|
+
# `Rigor::Analysis::Diagnostic`s through {#diagnostic}, absorbing
|
|
673
|
+
# the `violations.map { |v| diagnostic(node, …) }` block the
|
|
674
|
+
# node-rule plugins otherwise repeat. Each violation duck-types:
|
|
675
|
+
# `#message` (required); optional `#node` (the Prism node to
|
|
676
|
+
# position at — falls back to the `node:` argument, the common
|
|
677
|
+
# "all violations point at the same call" case), `#location` (a
|
|
678
|
+
# sub-location such as `node.message_loc`), `#severity` (defaults
|
|
679
|
+
# `:error`), and `#rule`. Returns an Array suitable for direct
|
|
680
|
+
# return from `#diagnostics_for_file` / a `node_rule` block.
|
|
681
|
+
def diagnostics_for(violations, path:, node: nil)
|
|
682
|
+
Array(violations).map do |violation|
|
|
683
|
+
target = (violation.node if violation.respond_to?(:node)) || node
|
|
684
|
+
diagnostic(
|
|
685
|
+
target,
|
|
686
|
+
path: path,
|
|
687
|
+
message: violation.message,
|
|
688
|
+
severity: (violation.respond_to?(:severity) && violation.severity) || :error,
|
|
689
|
+
rule: (violation.rule if violation.respond_to?(:rule)),
|
|
690
|
+
location: (violation.location if violation.respond_to?(:location))
|
|
691
|
+
)
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by
|
|
696
|
+
# another plugin's `#prepare` hook, memoised per `(plugin_id,
|
|
697
|
+
# name)` on this instance INCLUDING a nil result. The nil-inclusive
|
|
698
|
+
# memo retires the hand-rolled `@x_resolved` flag the discovery
|
|
699
|
+
# plugins carried to distinguish "fact not published" from "not yet
|
|
700
|
+
# read". `services.fact_store` is the only sanctioned cross-plugin
|
|
701
|
+
# channel; a fact no loaded producer published reads as nil.
|
|
702
|
+
def read_fact(plugin_id:, name:)
|
|
703
|
+
key = [plugin_id.to_s, name.to_sym].freeze
|
|
704
|
+
return @fact_cache[key] if @fact_cache.key?(key)
|
|
705
|
+
|
|
706
|
+
@fact_cache[key] = services.fact_store.read(plugin_id: plugin_id.to_s, name: name.to_sym)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# ADR-60 WD4 — runs a declared {.producer} through {#cache_for}
|
|
710
|
+
# and returns its value, memoised per `(id, params)` INCLUDING nil.
|
|
711
|
+
# A `StandardError` the producer raises (a malformed project file,
|
|
712
|
+
# an I/O failure) is rescued, recorded for {#producer_error}, and
|
|
713
|
+
# yields nil — so one bad project file degrades a plugin to silence
|
|
714
|
+
# rather than aborting the whole run. This is the `*_index_or_nil`
|
|
715
|
+
# shape the discovery plugins hand-rolled, named once.
|
|
716
|
+
def producer_value(id, params: {})
|
|
717
|
+
key = [id.to_sym, params].freeze
|
|
718
|
+
return @producer_value_cache[key] if @producer_value_cache.key?(key)
|
|
719
|
+
|
|
720
|
+
@producer_value_cache[key] = cache_for(id, params: params).call
|
|
721
|
+
rescue StandardError => e
|
|
722
|
+
@producer_errors[id.to_sym] = e
|
|
723
|
+
@producer_value_cache[key] = nil
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# ADR-60 WD4 — the `StandardError` a prior {#producer_value} call
|
|
727
|
+
# rescued for `id`, or nil when it succeeded or was never called.
|
|
728
|
+
# Plugins surface it as a load-error diagnostic from
|
|
729
|
+
# `#diagnostics_for_file`.
|
|
730
|
+
def producer_error(id)
|
|
731
|
+
@producer_errors[id.to_sym]
|
|
732
|
+
end
|
|
733
|
+
|
|
628
734
|
# Boilerplate-reduction helper (review §1.3): the "did you mean …?"
|
|
629
735
|
# suggestion every diagnostic-emitting plugin otherwise hand-rolls.
|
|
630
736
|
# Returns the closest of `candidates` to `name` via
|
|
@@ -695,32 +801,38 @@ module Rigor
|
|
|
695
801
|
@io_boundary ||= services.io_boundary_for(manifest.id)
|
|
696
802
|
end
|
|
697
803
|
|
|
698
|
-
# ADR-7 § "Slice 6-A" — returns a callable that
|
|
699
|
-
# a `Cache::Store#
|
|
700
|
-
# named producer
|
|
701
|
-
#
|
|
702
|
-
# `PluginEntry` template (id, version, config_hash)
|
|
703
|
-
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
804
|
+
# ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that
|
|
805
|
+
# performs a `Cache::Store#fetch_or_validate` round-trip for
|
|
806
|
+
# the named producer (the ADR-45 record-and-validate path).
|
|
807
|
+
# The entry is KEYED on the stable identity inputs — the
|
|
808
|
+
# plugin's `PluginEntry` template (id, version, config_hash)
|
|
809
|
+
# composed with the optional `descriptor:` extras — and
|
|
810
|
+
# stores, beside the value, a DEPENDENCY descriptor recorded
|
|
811
|
+
# AFTER the producer block ran: the {IoBoundary}'s
|
|
812
|
+
# post-compute read history plus the evaluated `watch:`
|
|
813
|
+
# {Cache::Descriptor::GlobEntry} rows. In-block reads are
|
|
814
|
+
# therefore always captured (the structural stale-cache
|
|
815
|
+
# hazard `fetch_or_compute`'s call-time snapshot carried);
|
|
816
|
+
# the next run re-validates the recorded dependencies by
|
|
817
|
+
# re-digest (`Descriptor#fresh?`) and recomputes when any
|
|
818
|
+
# changed. The producer id is auto-prefixed
|
|
819
|
+
# `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
|
|
820
|
+
# caches stay sandboxed from built-in producers.
|
|
707
821
|
#
|
|
708
822
|
# When `services.cache_store` is `nil` (e.g. CLI
|
|
709
823
|
# `--no-cache`), the callable bypasses the cache and
|
|
710
824
|
# runs the producer block every time — same semantics
|
|
711
825
|
# as the v0.0.9 cache surface for built-in producers.
|
|
712
826
|
#
|
|
713
|
-
# `descriptor:` (optional
|
|
714
|
-
#
|
|
715
|
-
#
|
|
716
|
-
#
|
|
717
|
-
#
|
|
718
|
-
#
|
|
719
|
-
#
|
|
720
|
-
#
|
|
721
|
-
#
|
|
722
|
-
# `Cache::Descriptor::Conflict` to make divergent inputs
|
|
723
|
-
# visible rather than silently shadowing.
|
|
827
|
+
# `descriptor:` (optional) supplies extra `Cache::Descriptor`
|
|
828
|
+
# rows for IDENTITY inputs — gem-version `GemEntry` pins,
|
|
829
|
+
# `ConfigEntry` rows for external state — that compose into
|
|
830
|
+
# the cache KEY via `Cache::Descriptor.compose`; per-slot
|
|
831
|
+
# conflicts raise `Cache::Descriptor::Conflict` to make
|
|
832
|
+
# divergent inputs visible rather than silently shadowing.
|
|
833
|
+
# A key change is a miss, so the invalidation effect of the
|
|
834
|
+
# legacy `glob_descriptor`-as-`descriptor:` idiom is
|
|
835
|
+
# preserved unchanged.
|
|
724
836
|
def cache_for(producer_id, params: {}, descriptor: nil)
|
|
725
837
|
producer = self.class.producers[producer_id.to_sym]
|
|
726
838
|
unless producer
|
|
@@ -733,16 +845,18 @@ module Rigor
|
|
|
733
845
|
return compute unless store
|
|
734
846
|
|
|
735
847
|
prefixed_id = "plugin.#{manifest.id}.#{producer_id}"
|
|
736
|
-
|
|
848
|
+
key_descriptor = compose_key_descriptor(descriptor)
|
|
737
849
|
lambda do
|
|
738
|
-
store.
|
|
850
|
+
store.fetch_or_validate(
|
|
739
851
|
producer_id: prefixed_id,
|
|
852
|
+
key_descriptor: key_descriptor,
|
|
740
853
|
params: params,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
854
|
+
serialize: pair_serializer(producer[:serialize]),
|
|
855
|
+
deserialize: pair_deserializer(producer[:deserialize])
|
|
856
|
+
) do
|
|
857
|
+
value = compute.call
|
|
858
|
+
[value, producer_dependency_descriptor(producer)]
|
|
859
|
+
end
|
|
746
860
|
end
|
|
747
861
|
end
|
|
748
862
|
|
|
@@ -754,31 +868,13 @@ module Rigor
|
|
|
754
868
|
# descriptor), or any removal (the previously-matched file
|
|
755
869
|
# drops out).
|
|
756
870
|
#
|
|
757
|
-
#
|
|
758
|
-
#
|
|
759
|
-
#
|
|
760
|
-
#
|
|
761
|
-
#
|
|
762
|
-
#
|
|
763
|
-
#
|
|
764
|
-
# warm runs return stale producer output when files have
|
|
765
|
-
# changed between sessions.
|
|
766
|
-
#
|
|
767
|
-
# Discovery-style producers (`actioncable`'s `:channel_index`,
|
|
768
|
-
# `actionmailer`'s `:mailer_index`, `rails-i18n`'s
|
|
769
|
-
# `:locale_index`) all follow the same pattern: walk a glob
|
|
770
|
-
# under one or more search roots, parse / read every match,
|
|
771
|
-
# build a typed index. They MUST call this helper at the
|
|
772
|
-
# `cache_for(descriptor: …)` site to be cache-correct under
|
|
773
|
-
# the persistent `Cache::Store` `rigor check` uses by
|
|
774
|
-
# default.
|
|
775
|
-
#
|
|
776
|
-
# The helper pays one SHA-256 read per matched file at
|
|
777
|
-
# call time; the producer block typically re-reads through
|
|
778
|
-
# `io_boundary.read_file` so the cost is doubled. For
|
|
779
|
-
# discovery globs in the 10-100 file range this is
|
|
780
|
-
# negligible (~ms) relative to the parse + walk the
|
|
781
|
-
# producer does on cache miss.
|
|
871
|
+
# ADR-60 WD3 made this **private**: the declared way for a
|
|
872
|
+
# discovery-style producer to cover its glob is `producer
|
|
873
|
+
# watch:` (one {Cache::Descriptor::GlobEntry} per glob in the
|
|
874
|
+
# record-and-validate dependency descriptor), not a hand-built
|
|
875
|
+
# descriptor composed into the cache *key*. The method survives
|
|
876
|
+
# only as the building block for the rare producer that needs
|
|
877
|
+
# `FileEntry` rows directly; plugin code calls `watch:`.
|
|
782
878
|
#
|
|
783
879
|
# @param roots [Array<String>] search roots (relative to
|
|
784
880
|
# the project root, or absolute paths)
|
|
@@ -798,6 +894,7 @@ module Rigor
|
|
|
798
894
|
end
|
|
799
895
|
Cache::Descriptor.new(files: entries)
|
|
800
896
|
end
|
|
897
|
+
private :glob_descriptor
|
|
801
898
|
|
|
802
899
|
private
|
|
803
900
|
|
|
@@ -923,17 +1020,6 @@ module Rigor
|
|
|
923
1020
|
matched.uniq.sort.select { |path| File.file?(path) }
|
|
924
1021
|
end
|
|
925
1022
|
|
|
926
|
-
# ADR-7 § "Slice 6-B" — composes the per-call cache
|
|
927
|
-
# descriptor from (1) the plugin's PluginEntry template
|
|
928
|
-
# and (2) the IoBoundary's accumulated FileEntry rows.
|
|
929
|
-
def build_plugin_cache_descriptor
|
|
930
|
-
boundary_descriptor = io_boundary.cache_descriptor
|
|
931
|
-
Cache::Descriptor.new(
|
|
932
|
-
plugins: [plugin_entry],
|
|
933
|
-
files: boundary_descriptor.files
|
|
934
|
-
)
|
|
935
|
-
end
|
|
936
|
-
|
|
937
1023
|
public
|
|
938
1024
|
|
|
939
1025
|
# ADR-32 WD5 — the `Cache::Descriptor::PluginEntry`
|
|
@@ -962,20 +1048,90 @@ module Rigor
|
|
|
962
1048
|
|
|
963
1049
|
private
|
|
964
1050
|
|
|
965
|
-
# ADR-
|
|
966
|
-
#
|
|
967
|
-
# extension
|
|
968
|
-
#
|
|
969
|
-
#
|
|
970
|
-
#
|
|
971
|
-
#
|
|
972
|
-
|
|
973
|
-
|
|
1051
|
+
# ADR-60 WD3 — the cache KEY descriptor: the plugin's
|
|
1052
|
+
# PluginEntry template composed with an optional
|
|
1053
|
+
# plugin-author-supplied extension carrying IDENTITY inputs
|
|
1054
|
+
# (gem-version pins, `ConfigEntry` rows, configuration-file
|
|
1055
|
+
# digests). The IoBoundary read history deliberately does NOT
|
|
1056
|
+
# enter the key — it is recorded post-compute into the
|
|
1057
|
+
# dependency descriptor instead (see
|
|
1058
|
+
# {#producer_dependency_descriptor}).
|
|
1059
|
+
def compose_key_descriptor(extra)
|
|
1060
|
+
auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
|
|
974
1061
|
return auto_built if extra.nil?
|
|
975
1062
|
|
|
976
1063
|
Cache::Descriptor.compose(auto_built, extra)
|
|
977
1064
|
end
|
|
978
1065
|
|
|
1066
|
+
# ADR-60 WD3 — the dependency descriptor stored beside the
|
|
1067
|
+
# producer's value, built AFTER the block ran so every
|
|
1068
|
+
# in-block `io_boundary` read is captured, plus the evaluated
|
|
1069
|
+
# `watch:` glob rows.
|
|
1070
|
+
#
|
|
1071
|
+
# The boundary snapshot may carry `ConfigEntry` rows (URL
|
|
1072
|
+
# fetches, see {IoBoundary#open_url}). `Descriptor#fresh?`
|
|
1073
|
+
# refuses any non-file/glob slot, so including them makes the
|
|
1074
|
+
# entry permanently stale → the producer recomputes EVERY run.
|
|
1075
|
+
# That is deliberate: it is sound (never stale) and
|
|
1076
|
+
# URL-reading producers are rare; a remote document has no
|
|
1077
|
+
# cheap local re-validation anyway.
|
|
1078
|
+
def producer_dependency_descriptor(producer)
|
|
1079
|
+
boundary = io_boundary.cache_descriptor
|
|
1080
|
+
Cache::Descriptor.new(
|
|
1081
|
+
files: boundary.files,
|
|
1082
|
+
configs: boundary.configs,
|
|
1083
|
+
globs: watch_glob_entries(producer[:watch])
|
|
1084
|
+
)
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
# ADR-60 WD3 — evaluates a producer's `watch:` declaration
|
|
1088
|
+
# into {Cache::Descriptor::GlobEntry} rows. A Proc is
|
|
1089
|
+
# `instance_exec`'d on this plugin instance (so `#init`-built
|
|
1090
|
+
# search roots are in scope); the result — like the static
|
|
1091
|
+
# form — is an Array of `[roots, pattern, ...]` tuples, one
|
|
1092
|
+
# GlobEntry per (root, pattern) pair. Roots are expanded to
|
|
1093
|
+
# absolute paths (matching {#glob_descriptor}) so freshness
|
|
1094
|
+
# re-validation does not depend on the validating process's
|
|
1095
|
+
# working directory.
|
|
1096
|
+
def watch_glob_entries(watch)
|
|
1097
|
+
return [] if watch.nil?
|
|
1098
|
+
|
|
1099
|
+
tuples = watch.respond_to?(:call) ? instance_exec(&watch) : watch
|
|
1100
|
+
Array(tuples).flat_map do |tuple|
|
|
1101
|
+
roots, *patterns = Array(tuple)
|
|
1102
|
+
Array(roots).flat_map do |root|
|
|
1103
|
+
absolute = File.expand_path(root.to_s)
|
|
1104
|
+
patterns.map { |pattern| Cache::Descriptor::GlobEntry.compute(root: absolute, pattern: pattern.to_s) }
|
|
1105
|
+
end
|
|
1106
|
+
end.uniq
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
# ADR-60 WD3 — `fetch_or_validate` stores a
|
|
1110
|
+
# `[value, dependency_descriptor]` pair, but the producer's
|
|
1111
|
+
# declared `serialize:`/`deserialize:` contract covers the
|
|
1112
|
+
# VALUE alone. These wrappers apply the custom callable to the
|
|
1113
|
+
# value half and Marshal the descriptor half, so a producer
|
|
1114
|
+
# with a non-Marshal-clean value keeps working unchanged. A
|
|
1115
|
+
# nil callable returns nil — the store's default whole-pair
|
|
1116
|
+
# Marshal round-trip applies.
|
|
1117
|
+
def pair_serializer(serialize)
|
|
1118
|
+
return nil if serialize.nil?
|
|
1119
|
+
|
|
1120
|
+
lambda do |pair|
|
|
1121
|
+
value, dependency_descriptor = pair
|
|
1122
|
+
Marshal.dump([serialize.call(value).b, Marshal.dump(dependency_descriptor)]).b
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
def pair_deserializer(deserialize)
|
|
1127
|
+
return nil if deserialize.nil?
|
|
1128
|
+
|
|
1129
|
+
lambda do |bytes|
|
|
1130
|
+
value_bytes, descriptor_bytes = Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
|
|
1131
|
+
[deserialize.call(value_bytes), Marshal.load(descriptor_bytes)] # rubocop:disable Security/MarshalLoad
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
|
|
979
1135
|
def digest_config(config)
|
|
980
1136
|
canonical = Cache::Descriptor.canonicalize_value(config || {})
|
|
981
1137
|
Digest::SHA256.hexdigest(JSON.generate(canonical))
|
|
@@ -4,9 +4,9 @@ module Rigor
|
|
|
4
4
|
module Plugin
|
|
5
5
|
module Macro
|
|
6
6
|
# ADR-16 Tier A declaration: "the block passed to a
|
|
7
|
-
# class-level DSL call of one of `
|
|
8
|
-
# method on `receiver_constraint`'s subclass tree,
|
|
9
|
-
# `self` typed accordingly."
|
|
7
|
+
# class-level DSL call of one of `method_names` runs as an
|
|
8
|
+
# instance method on `receiver_constraint`'s subclass tree,
|
|
9
|
+
# with `self` typed accordingly."
|
|
10
10
|
#
|
|
11
11
|
# Authored on a plugin manifest:
|
|
12
12
|
#
|
|
@@ -16,7 +16,7 @@ module Rigor
|
|
|
16
16
|
# block_as_methods: [
|
|
17
17
|
# Rigor::Plugin::Macro::BlockAsMethod.new(
|
|
18
18
|
# receiver_constraint: "Sinatra::Base",
|
|
19
|
-
#
|
|
19
|
+
# method_names: %i[get post put delete head options patch link unlink]
|
|
20
20
|
# )
|
|
21
21
|
# ]
|
|
22
22
|
# )
|
|
@@ -41,9 +41,10 @@ module Rigor
|
|
|
41
41
|
# for the entry to fire. For Sinatra modular-style this is
|
|
42
42
|
# `"Sinatra::Base"`; the substrate's class-context match
|
|
43
43
|
# accepts every subclass.
|
|
44
|
-
# - `
|
|
44
|
+
# - `method_names` — Array of Symbol method names. A call shape
|
|
45
45
|
# `<receiver_subclass>.get('/path') { ... }` matches when
|
|
46
|
-
# `:get` is in this list.
|
|
46
|
+
# `:get` is in this list. (Named `verbs:` before ADR-60 WD2
|
|
47
|
+
# normalised the macro value-object vocabulary.)
|
|
47
48
|
# - `self_type` — Symbol selecting the kind of `self`-binding
|
|
48
49
|
# the substrate applies inside the block. Slice 1a accepts
|
|
49
50
|
# only `:receiver_instance` (the block runs as an instance
|
|
@@ -53,22 +54,22 @@ module Rigor
|
|
|
53
54
|
# ## Ractor-shareability
|
|
54
55
|
#
|
|
55
56
|
# All fields are frozen at construction (ADR-15 Phase 1).
|
|
56
|
-
# `
|
|
57
|
-
# not leak into the value. `Ractor.shareable?` returns
|
|
58
|
-
# after `#initialize`.
|
|
57
|
+
# `method_names` is dup-frozen so the caller's mutable array
|
|
58
|
+
# does not leak into the value. `Ractor.shareable?` returns
|
|
59
|
+
# true after `#initialize`.
|
|
59
60
|
class BlockAsMethod
|
|
60
61
|
SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
|
|
61
62
|
VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
|
|
62
63
|
|
|
63
|
-
attr_reader :receiver_constraint, :
|
|
64
|
+
attr_reader :receiver_constraint, :method_names, :self_type
|
|
64
65
|
|
|
65
|
-
def initialize(receiver_constraint:,
|
|
66
|
+
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
|
|
66
67
|
validate_receiver_constraint!(receiver_constraint)
|
|
67
|
-
|
|
68
|
+
validate_method_names!(method_names)
|
|
68
69
|
validate_self_type!(self_type)
|
|
69
70
|
|
|
70
71
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
71
|
-
@
|
|
72
|
+
@method_names = method_names.map(&:to_sym).freeze
|
|
72
73
|
@self_type = self_type
|
|
73
74
|
freeze
|
|
74
75
|
end
|
|
@@ -76,7 +77,7 @@ module Rigor
|
|
|
76
77
|
def to_h
|
|
77
78
|
{
|
|
78
79
|
"receiver_constraint" => receiver_constraint,
|
|
79
|
-
"
|
|
80
|
+
"method_names" => method_names.map(&:to_s),
|
|
80
81
|
"self_type" => self_type.to_s
|
|
81
82
|
}
|
|
82
83
|
end
|
|
@@ -84,13 +85,13 @@ module Rigor
|
|
|
84
85
|
def ==(other)
|
|
85
86
|
other.is_a?(BlockAsMethod) &&
|
|
86
87
|
receiver_constraint == other.receiver_constraint &&
|
|
87
|
-
|
|
88
|
+
method_names == other.method_names &&
|
|
88
89
|
self_type == other.self_type
|
|
89
90
|
end
|
|
90
91
|
alias eql? ==
|
|
91
92
|
|
|
92
93
|
def hash
|
|
93
|
-
[receiver_constraint,
|
|
94
|
+
[receiver_constraint, method_names, self_type].hash
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
private
|
|
@@ -103,17 +104,17 @@ module Rigor
|
|
|
103
104
|
"got #{value.inspect}"
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def
|
|
107
|
-
unless
|
|
107
|
+
def validate_method_names!(method_names)
|
|
108
|
+
unless method_names.is_a?(Array) && !method_names.empty?
|
|
108
109
|
raise ArgumentError,
|
|
109
|
-
"Plugin::Macro::BlockAsMethod#
|
|
110
|
+
"Plugin::Macro::BlockAsMethod#method_names must be a non-empty Array, got #{method_names.inspect}"
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
method_names.each do |v|
|
|
113
114
|
next if v.is_a?(Symbol) || (v.is_a?(String) && !v.empty?)
|
|
114
115
|
|
|
115
116
|
raise ArgumentError,
|
|
116
|
-
"Plugin::Macro::BlockAsMethod#
|
|
117
|
+
"Plugin::Macro::BlockAsMethod#method_names entries must be Symbol/non-empty String, " \
|
|
117
118
|
"got #{v.inspect}"
|
|
118
119
|
end
|
|
119
120
|
end
|
|
@@ -33,7 +33,7 @@ module Rigor
|
|
|
33
33
|
# receiver_constraint: "Mangrove::Enum", # `extend`-ed marker module
|
|
34
34
|
# block_method: :variants, # the enclosing DSL block
|
|
35
35
|
# variant_method: :variant, # each declaration call
|
|
36
|
-
#
|
|
36
|
+
# symbol_arg_position: 0, # constant arg → nested class
|
|
37
37
|
# inner_arg_position: 1, # type arg → `#inner` return
|
|
38
38
|
# inner_reader: :inner # the payload reader name
|
|
39
39
|
# )
|
|
@@ -48,8 +48,10 @@ module Rigor
|
|
|
48
48
|
# (`:variants`).
|
|
49
49
|
# - `variant_method` — Symbol naming each declaration call
|
|
50
50
|
# inside the block (`:variant`).
|
|
51
|
-
# - `
|
|
51
|
+
# - `symbol_arg_position` — Integer (default 0): the argument
|
|
52
52
|
# index whose literal **constant** names the nested subclass.
|
|
53
|
+
# (Named `name_arg_position:` before ADR-60 WD2 normalised
|
|
54
|
+
# the macro value-object vocabulary.)
|
|
53
55
|
# - `inner_arg_position` — Integer (default 1): the argument
|
|
54
56
|
# index whose type expression becomes the `#inner` reader's
|
|
55
57
|
# return type. Slice A resolves a constant type argument
|
|
@@ -69,21 +71,21 @@ module Rigor
|
|
|
69
71
|
# `Environment#class_ordering`.
|
|
70
72
|
class NestedClassTemplate
|
|
71
73
|
attr_reader :receiver_constraint, :block_method, :variant_method,
|
|
72
|
-
:
|
|
74
|
+
:symbol_arg_position, :inner_arg_position, :inner_reader
|
|
73
75
|
|
|
74
76
|
def initialize(receiver_constraint:, block_method: :variants, variant_method: :variant,
|
|
75
|
-
|
|
77
|
+
symbol_arg_position: 0, inner_arg_position: 1, inner_reader: :inner)
|
|
76
78
|
validate_constraint!(receiver_constraint)
|
|
77
79
|
validate_method!(block_method, "block_method")
|
|
78
80
|
validate_method!(variant_method, "variant_method")
|
|
79
|
-
validate_position!(
|
|
81
|
+
validate_position!(symbol_arg_position, "symbol_arg_position")
|
|
80
82
|
validate_position!(inner_arg_position, "inner_arg_position")
|
|
81
83
|
validate_method!(inner_reader, "inner_reader")
|
|
82
84
|
|
|
83
85
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
84
86
|
@block_method = block_method.to_sym
|
|
85
87
|
@variant_method = variant_method.to_sym
|
|
86
|
-
@
|
|
88
|
+
@symbol_arg_position = symbol_arg_position
|
|
87
89
|
@inner_arg_position = inner_arg_position
|
|
88
90
|
@inner_reader = inner_reader.to_sym
|
|
89
91
|
freeze
|
|
@@ -94,7 +96,7 @@ module Rigor
|
|
|
94
96
|
"receiver_constraint" => receiver_constraint,
|
|
95
97
|
"block_method" => block_method.to_s,
|
|
96
98
|
"variant_method" => variant_method.to_s,
|
|
97
|
-
"
|
|
99
|
+
"symbol_arg_position" => symbol_arg_position,
|
|
98
100
|
"inner_arg_position" => inner_arg_position,
|
|
99
101
|
"inner_reader" => inner_reader.to_s
|
|
100
102
|
}
|
data/lib/rigor/plugin/macro.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "macro/block_as_method"
|
|
4
|
-
require_relative "macro/external_file"
|
|
5
4
|
require_relative "macro/heredoc_template"
|
|
6
5
|
require_relative "macro/nested_class_template"
|
|
7
6
|
require_relative "macro/trait_registry"
|
|
@@ -11,8 +10,8 @@ module Rigor
|
|
|
11
10
|
# Substrate declarations for the macro / DSL expansion tiers
|
|
12
11
|
# introduced by ADR-16. Plugin authors declare entries under
|
|
13
12
|
# `Plugin::Manifest` slots (`block_as_methods:`,
|
|
14
|
-
# `trait_registries:`, `
|
|
15
|
-
# `
|
|
13
|
+
# `trait_registries:`, `heredoc_templates:`,
|
|
14
|
+
# `nested_class_templates:`) and the substrate consumes them
|
|
16
15
|
# to recognise the call shapes a library exposes to its users.
|
|
17
16
|
#
|
|
18
17
|
# Slice 1a (this file's first delivery) ships the Tier A value
|