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
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -18,10 +18,11 @@ module Rigor
|
|
|
18
18
|
# overrides {#init} to wire up any state it needs from the
|
|
19
19
|
# injected service container.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# contributions
|
|
24
|
-
#
|
|
21
|
+
# This class implements all plugin protocol hooks: per-call
|
|
22
|
+
# return-type contributions (`dynamic_return`), narrowing-fact
|
|
23
|
+
# contributions (`type_specifier`), AST node rules (`node_rule`),
|
|
24
|
+
# and producer/cache hooks. Cumulative implementation per the
|
|
25
|
+
# ADR-37 / ADR-52 slice chain.
|
|
25
26
|
#
|
|
26
27
|
# Example plugin:
|
|
27
28
|
#
|
|
@@ -74,23 +75,59 @@ module Rigor
|
|
|
74
75
|
# argument; the same params Hash mixes into the cache
|
|
75
76
|
# key per `Cache::Descriptor#cache_key_for`.
|
|
76
77
|
#
|
|
77
|
-
# `serialize:` / `deserialize:`
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
78
|
+
# `serialize:` / `deserialize:` apply to the producer's
|
|
79
|
+
# return VALUE (the cache layer wraps them around the
|
|
80
|
+
# record-and-validate entry pair itself). Default
|
|
81
|
+
# round-trip is `Marshal.dump` / `Marshal.load` per the
|
|
82
|
+
# v0.0.9 callable surface; producers whose return values
|
|
83
|
+
# are not Marshal-clean must supply their own pair.
|
|
84
|
+
#
|
|
85
|
+
# `watch:` (ADR-60 WD3) declares the glob coverage of a
|
|
86
|
+
# discovery-style producer — the files whose addition /
|
|
87
|
+
# removal / edit must invalidate the cached value even
|
|
88
|
+
# when the producer block never read them individually
|
|
89
|
+
# (e.g. it globbed a directory itself). It is either
|
|
90
|
+
#
|
|
91
|
+
# - a static Array of `[roots, pattern, ...]` tuples
|
|
92
|
+
# (`roots` a String or Array of Strings; one or more
|
|
93
|
+
# glob-pattern suffixes per tuple — the same shape
|
|
94
|
+
# {#glob_descriptor} takes), or
|
|
95
|
+
# - a Proc, run through `instance_exec` on the plugin
|
|
96
|
+
# instance at `cache_for` invocation time (NEVER at
|
|
97
|
+
# class-definition time — search roots are typically
|
|
98
|
+
# computed in `#init` from config), returning the same
|
|
99
|
+
# tuple Array.
|
|
100
|
+
#
|
|
101
|
+
# The evaluated tuples become {Cache::Descriptor::GlobEntry}
|
|
102
|
+
# rows in the dependency descriptor recorded after the
|
|
103
|
+
# block runs; `Descriptor#fresh?` re-globs + re-digests on
|
|
104
|
+
# the next run.
|
|
82
105
|
#
|
|
83
106
|
# Producer ids are auto-prefixed `plugin.<manifest.id>.`
|
|
84
107
|
# at the cache layer (slice 6-C) so plugin-side ids cannot
|
|
85
108
|
# collide with built-in producers.
|
|
86
|
-
def producer(id, serialize: nil, deserialize: nil, &block)
|
|
109
|
+
def producer(id, watch: nil, serialize: nil, deserialize: nil, &block)
|
|
87
110
|
raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?
|
|
88
111
|
|
|
112
|
+
validate_producer_watch!(watch)
|
|
89
113
|
@producers ||= {}
|
|
90
|
-
@producers[id.to_sym] = {
|
|
114
|
+
@producers[id.to_sym] = {
|
|
115
|
+
block: block, watch: watch, serialize: serialize, deserialize: deserialize
|
|
116
|
+
}.freeze
|
|
91
117
|
id.to_sym
|
|
92
118
|
end
|
|
93
119
|
|
|
120
|
+
# ADR-60 WD3 — `watch:` is nil (no glob coverage), a static
|
|
121
|
+
# tuple Array, or a Proc evaluated per `cache_for` call.
|
|
122
|
+
def validate_producer_watch!(watch)
|
|
123
|
+
return if watch.nil? || watch.is_a?(Array) || watch.respond_to?(:call)
|
|
124
|
+
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"Plugin::Base.producer watch: must be nil, an Array of [roots, pattern, ...] tuples, " \
|
|
127
|
+
"or a Proc returning one, got #{watch.inspect}"
|
|
128
|
+
end
|
|
129
|
+
private :validate_producer_watch!
|
|
130
|
+
|
|
94
131
|
# Frozen snapshot of the producer table. Inherited
|
|
95
132
|
# producers from a superclass are intentionally NOT
|
|
96
133
|
# surfaced — Plugin::Base subclasses do not chain
|
|
@@ -445,6 +482,13 @@ module Rigor
|
|
|
445
482
|
# memo-on-first-dispatch is a Hash-content mutation, sound even on
|
|
446
483
|
# a self-freezing plugin.
|
|
447
484
|
@dynamic_return_runtime_cache = {}
|
|
485
|
+
# ADR-60 WD4 — nil-inclusive memo tables for the authoring
|
|
486
|
+
# helpers ({#read_fact} / {#producer_value} / {#producer_error}).
|
|
487
|
+
# Allocated here, before any subclass `initialize` self-freeze,
|
|
488
|
+
# for the same reason: a populate is a Hash-content mutation.
|
|
489
|
+
@fact_cache = {}
|
|
490
|
+
@producer_value_cache = {}
|
|
491
|
+
@producer_errors = {}
|
|
448
492
|
end
|
|
449
493
|
|
|
450
494
|
# Override in subclasses to wire any state the plugin needs
|
|
@@ -500,10 +544,11 @@ module Rigor
|
|
|
500
544
|
#
|
|
501
545
|
# `path` is the analysed file path; `scope` is the entry
|
|
502
546
|
# `Rigor::Scope` after `ScopeIndexer` ran; `root` is the
|
|
503
|
-
# parsed `Prism::Node` root. Plugin authors traverse
|
|
504
|
-
# themselves
|
|
505
|
-
#
|
|
506
|
-
#
|
|
547
|
+
# parsed `Prism::Node` root. Plugin authors can traverse
|
|
548
|
+
# `root` themselves or declare rules via the `.node_rule` DSL
|
|
549
|
+
# (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base
|
|
550
|
+
# class mentioned in ADR-2 was superseded by the block-based
|
|
551
|
+
# `.node_rule` DSL.
|
|
507
552
|
#
|
|
508
553
|
# Default returns `[]` so plugins that contribute through
|
|
509
554
|
# other channels (e.g. slice-4 narrowing contributions,
|
|
@@ -625,6 +670,69 @@ module Rigor
|
|
|
625
670
|
)
|
|
626
671
|
end
|
|
627
672
|
|
|
673
|
+
# ADR-60 WD4 — maps a plugin's own violation objects to
|
|
674
|
+
# `Rigor::Analysis::Diagnostic`s through {#diagnostic}, absorbing
|
|
675
|
+
# the `violations.map { |v| diagnostic(node, …) }` block the
|
|
676
|
+
# node-rule plugins otherwise repeat. Each violation duck-types:
|
|
677
|
+
# `#message` (required); optional `#node` (the Prism node to
|
|
678
|
+
# position at — falls back to the `node:` argument, the common
|
|
679
|
+
# "all violations point at the same call" case), `#location` (a
|
|
680
|
+
# sub-location such as `node.message_loc`), `#severity` (defaults
|
|
681
|
+
# `:error`), and `#rule`. Returns an Array suitable for direct
|
|
682
|
+
# return from `#diagnostics_for_file` / a `node_rule` block.
|
|
683
|
+
def diagnostics_for(violations, path:, node: nil)
|
|
684
|
+
Array(violations).map do |violation|
|
|
685
|
+
target = (violation.node if violation.respond_to?(:node)) || node
|
|
686
|
+
diagnostic(
|
|
687
|
+
target,
|
|
688
|
+
path: path,
|
|
689
|
+
message: violation.message,
|
|
690
|
+
severity: (violation.respond_to?(:severity) && violation.severity) || :error,
|
|
691
|
+
rule: (violation.rule if violation.respond_to?(:rule)),
|
|
692
|
+
location: (violation.location if violation.respond_to?(:location))
|
|
693
|
+
)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by
|
|
698
|
+
# another plugin's `#prepare` hook, memoised per `(plugin_id,
|
|
699
|
+
# name)` on this instance INCLUDING a nil result. The nil-inclusive
|
|
700
|
+
# memo retires the hand-rolled `@x_resolved` flag the discovery
|
|
701
|
+
# plugins carried to distinguish "fact not published" from "not yet
|
|
702
|
+
# read". `services.fact_store` is the only sanctioned cross-plugin
|
|
703
|
+
# channel; a fact no loaded producer published reads as nil.
|
|
704
|
+
def read_fact(plugin_id:, name:)
|
|
705
|
+
key = [plugin_id.to_s, name.to_sym].freeze
|
|
706
|
+
return @fact_cache[key] if @fact_cache.key?(key)
|
|
707
|
+
|
|
708
|
+
@fact_cache[key] = services.fact_store.read(plugin_id: plugin_id.to_s, name: name.to_sym)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# ADR-60 WD4 — runs a declared {.producer} through {#cache_for}
|
|
712
|
+
# and returns its value, memoised per `(id, params)` INCLUDING nil.
|
|
713
|
+
# A `StandardError` the producer raises (a malformed project file,
|
|
714
|
+
# an I/O failure) is rescued, recorded for {#producer_error}, and
|
|
715
|
+
# yields nil — so one bad project file degrades a plugin to silence
|
|
716
|
+
# rather than aborting the whole run. This is the `*_index_or_nil`
|
|
717
|
+
# shape the discovery plugins hand-rolled, named once.
|
|
718
|
+
def producer_value(id, params: {})
|
|
719
|
+
key = [id.to_sym, params].freeze
|
|
720
|
+
return @producer_value_cache[key] if @producer_value_cache.key?(key)
|
|
721
|
+
|
|
722
|
+
@producer_value_cache[key] = cache_for(id, params: params).call
|
|
723
|
+
rescue StandardError => e
|
|
724
|
+
@producer_errors[id.to_sym] = e
|
|
725
|
+
@producer_value_cache[key] = nil
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# ADR-60 WD4 — the `StandardError` a prior {#producer_value} call
|
|
729
|
+
# rescued for `id`, or nil when it succeeded or was never called.
|
|
730
|
+
# Plugins surface it as a load-error diagnostic from
|
|
731
|
+
# `#diagnostics_for_file`.
|
|
732
|
+
def producer_error(id)
|
|
733
|
+
@producer_errors[id.to_sym]
|
|
734
|
+
end
|
|
735
|
+
|
|
628
736
|
# Boilerplate-reduction helper (review §1.3): the "did you mean …?"
|
|
629
737
|
# suggestion every diagnostic-emitting plugin otherwise hand-rolls.
|
|
630
738
|
# Returns the closest of `candidates` to `name` via
|
|
@@ -695,32 +803,38 @@ module Rigor
|
|
|
695
803
|
@io_boundary ||= services.io_boundary_for(manifest.id)
|
|
696
804
|
end
|
|
697
805
|
|
|
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
|
-
#
|
|
806
|
+
# ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that
|
|
807
|
+
# performs a `Cache::Store#fetch_or_validate` round-trip for
|
|
808
|
+
# the named producer (the ADR-45 record-and-validate path).
|
|
809
|
+
# The entry is KEYED on the stable identity inputs — the
|
|
810
|
+
# plugin's `PluginEntry` template (id, version, config_hash)
|
|
811
|
+
# composed with the optional `descriptor:` extras — and
|
|
812
|
+
# stores, beside the value, a DEPENDENCY descriptor recorded
|
|
813
|
+
# AFTER the producer block ran: the {IoBoundary}'s
|
|
814
|
+
# post-compute read history plus the evaluated `watch:`
|
|
815
|
+
# {Cache::Descriptor::GlobEntry} rows. In-block reads are
|
|
816
|
+
# therefore always captured (the structural stale-cache
|
|
817
|
+
# hazard `fetch_or_compute`'s call-time snapshot carried);
|
|
818
|
+
# the next run re-validates the recorded dependencies by
|
|
819
|
+
# re-digest (`Descriptor#fresh?`) and recomputes when any
|
|
820
|
+
# changed. The producer id is auto-prefixed
|
|
821
|
+
# `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
|
|
822
|
+
# caches stay sandboxed from built-in producers.
|
|
707
823
|
#
|
|
708
824
|
# When `services.cache_store` is `nil` (e.g. CLI
|
|
709
825
|
# `--no-cache`), the callable bypasses the cache and
|
|
710
826
|
# runs the producer block every time — same semantics
|
|
711
827
|
# as the v0.0.9 cache surface for built-in producers.
|
|
712
828
|
#
|
|
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.
|
|
829
|
+
# `descriptor:` (optional) supplies extra `Cache::Descriptor`
|
|
830
|
+
# rows for IDENTITY inputs — gem-version `GemEntry` pins,
|
|
831
|
+
# `ConfigEntry` rows for external state — that compose into
|
|
832
|
+
# the cache KEY via `Cache::Descriptor.compose`; per-slot
|
|
833
|
+
# conflicts raise `Cache::Descriptor::Conflict` to make
|
|
834
|
+
# divergent inputs visible rather than silently shadowing.
|
|
835
|
+
# A key change is a miss, so the invalidation effect of the
|
|
836
|
+
# legacy `glob_descriptor`-as-`descriptor:` idiom is
|
|
837
|
+
# preserved unchanged.
|
|
724
838
|
def cache_for(producer_id, params: {}, descriptor: nil)
|
|
725
839
|
producer = self.class.producers[producer_id.to_sym]
|
|
726
840
|
unless producer
|
|
@@ -733,16 +847,18 @@ module Rigor
|
|
|
733
847
|
return compute unless store
|
|
734
848
|
|
|
735
849
|
prefixed_id = "plugin.#{manifest.id}.#{producer_id}"
|
|
736
|
-
|
|
850
|
+
key_descriptor = compose_key_descriptor(descriptor)
|
|
737
851
|
lambda do
|
|
738
|
-
store.
|
|
852
|
+
store.fetch_or_validate(
|
|
739
853
|
producer_id: prefixed_id,
|
|
854
|
+
key_descriptor: key_descriptor,
|
|
740
855
|
params: params,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
856
|
+
serialize: pair_serializer(producer[:serialize]),
|
|
857
|
+
deserialize: pair_deserializer(producer[:deserialize])
|
|
858
|
+
) do
|
|
859
|
+
value = compute.call
|
|
860
|
+
[value, producer_dependency_descriptor(producer)]
|
|
861
|
+
end
|
|
746
862
|
end
|
|
747
863
|
end
|
|
748
864
|
|
|
@@ -754,31 +870,13 @@ module Rigor
|
|
|
754
870
|
# descriptor), or any removal (the previously-matched file
|
|
755
871
|
# drops out).
|
|
756
872
|
#
|
|
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.
|
|
873
|
+
# ADR-60 WD3 made this **private**: the declared way for a
|
|
874
|
+
# discovery-style producer to cover its glob is `producer
|
|
875
|
+
# watch:` (one {Cache::Descriptor::GlobEntry} per glob in the
|
|
876
|
+
# record-and-validate dependency descriptor), not a hand-built
|
|
877
|
+
# descriptor composed into the cache *key*. The method survives
|
|
878
|
+
# only as the building block for the rare producer that needs
|
|
879
|
+
# `FileEntry` rows directly; plugin code calls `watch:`.
|
|
782
880
|
#
|
|
783
881
|
# @param roots [Array<String>] search roots (relative to
|
|
784
882
|
# the project root, or absolute paths)
|
|
@@ -798,6 +896,7 @@ module Rigor
|
|
|
798
896
|
end
|
|
799
897
|
Cache::Descriptor.new(files: entries)
|
|
800
898
|
end
|
|
899
|
+
private :glob_descriptor
|
|
801
900
|
|
|
802
901
|
private
|
|
803
902
|
|
|
@@ -923,17 +1022,6 @@ module Rigor
|
|
|
923
1022
|
matched.uniq.sort.select { |path| File.file?(path) }
|
|
924
1023
|
end
|
|
925
1024
|
|
|
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
1025
|
public
|
|
938
1026
|
|
|
939
1027
|
# ADR-32 WD5 — the `Cache::Descriptor::PluginEntry`
|
|
@@ -962,20 +1050,90 @@ module Rigor
|
|
|
962
1050
|
|
|
963
1051
|
private
|
|
964
1052
|
|
|
965
|
-
# ADR-
|
|
966
|
-
#
|
|
967
|
-
# extension
|
|
968
|
-
#
|
|
969
|
-
#
|
|
970
|
-
#
|
|
971
|
-
#
|
|
972
|
-
|
|
973
|
-
|
|
1053
|
+
# ADR-60 WD3 — the cache KEY descriptor: the plugin's
|
|
1054
|
+
# PluginEntry template composed with an optional
|
|
1055
|
+
# plugin-author-supplied extension carrying IDENTITY inputs
|
|
1056
|
+
# (gem-version pins, `ConfigEntry` rows, configuration-file
|
|
1057
|
+
# digests). The IoBoundary read history deliberately does NOT
|
|
1058
|
+
# enter the key — it is recorded post-compute into the
|
|
1059
|
+
# dependency descriptor instead (see
|
|
1060
|
+
# {#producer_dependency_descriptor}).
|
|
1061
|
+
def compose_key_descriptor(extra)
|
|
1062
|
+
auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
|
|
974
1063
|
return auto_built if extra.nil?
|
|
975
1064
|
|
|
976
1065
|
Cache::Descriptor.compose(auto_built, extra)
|
|
977
1066
|
end
|
|
978
1067
|
|
|
1068
|
+
# ADR-60 WD3 — the dependency descriptor stored beside the
|
|
1069
|
+
# producer's value, built AFTER the block ran so every
|
|
1070
|
+
# in-block `io_boundary` read is captured, plus the evaluated
|
|
1071
|
+
# `watch:` glob rows.
|
|
1072
|
+
#
|
|
1073
|
+
# The boundary snapshot may carry `ConfigEntry` rows (URL
|
|
1074
|
+
# fetches, see {IoBoundary#open_url}). `Descriptor#fresh?`
|
|
1075
|
+
# refuses any non-file/glob slot, so including them makes the
|
|
1076
|
+
# entry permanently stale → the producer recomputes EVERY run.
|
|
1077
|
+
# That is deliberate: it is sound (never stale) and
|
|
1078
|
+
# URL-reading producers are rare; a remote document has no
|
|
1079
|
+
# cheap local re-validation anyway.
|
|
1080
|
+
def producer_dependency_descriptor(producer)
|
|
1081
|
+
boundary = io_boundary.cache_descriptor
|
|
1082
|
+
Cache::Descriptor.new(
|
|
1083
|
+
files: boundary.files,
|
|
1084
|
+
configs: boundary.configs,
|
|
1085
|
+
globs: watch_glob_entries(producer[:watch])
|
|
1086
|
+
)
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
# ADR-60 WD3 — evaluates a producer's `watch:` declaration
|
|
1090
|
+
# into {Cache::Descriptor::GlobEntry} rows. A Proc is
|
|
1091
|
+
# `instance_exec`'d on this plugin instance (so `#init`-built
|
|
1092
|
+
# search roots are in scope); the result — like the static
|
|
1093
|
+
# form — is an Array of `[roots, pattern, ...]` tuples, one
|
|
1094
|
+
# GlobEntry per (root, pattern) pair. Roots are expanded to
|
|
1095
|
+
# absolute paths (matching {#glob_descriptor}) so freshness
|
|
1096
|
+
# re-validation does not depend on the validating process's
|
|
1097
|
+
# working directory.
|
|
1098
|
+
def watch_glob_entries(watch)
|
|
1099
|
+
return [] if watch.nil?
|
|
1100
|
+
|
|
1101
|
+
tuples = watch.respond_to?(:call) ? instance_exec(&watch) : watch
|
|
1102
|
+
Array(tuples).flat_map do |tuple|
|
|
1103
|
+
roots, *patterns = Array(tuple)
|
|
1104
|
+
Array(roots).flat_map do |root|
|
|
1105
|
+
absolute = File.expand_path(root.to_s)
|
|
1106
|
+
patterns.map { |pattern| Cache::Descriptor::GlobEntry.compute(root: absolute, pattern: pattern.to_s) }
|
|
1107
|
+
end
|
|
1108
|
+
end.uniq
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
# ADR-60 WD3 — `fetch_or_validate` stores a
|
|
1112
|
+
# `[value, dependency_descriptor]` pair, but the producer's
|
|
1113
|
+
# declared `serialize:`/`deserialize:` contract covers the
|
|
1114
|
+
# VALUE alone. These wrappers apply the custom callable to the
|
|
1115
|
+
# value half and Marshal the descriptor half, so a producer
|
|
1116
|
+
# with a non-Marshal-clean value keeps working unchanged. A
|
|
1117
|
+
# nil callable returns nil — the store's default whole-pair
|
|
1118
|
+
# Marshal round-trip applies.
|
|
1119
|
+
def pair_serializer(serialize)
|
|
1120
|
+
return nil if serialize.nil?
|
|
1121
|
+
|
|
1122
|
+
lambda do |pair|
|
|
1123
|
+
value, dependency_descriptor = pair
|
|
1124
|
+
Marshal.dump([serialize.call(value).b, Marshal.dump(dependency_descriptor)]).b
|
|
1125
|
+
end
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
def pair_deserializer(deserialize)
|
|
1129
|
+
return nil if deserialize.nil?
|
|
1130
|
+
|
|
1131
|
+
lambda do |bytes|
|
|
1132
|
+
value_bytes, descriptor_bytes = Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
|
|
1133
|
+
[deserialize.call(value_bytes), Marshal.load(descriptor_bytes)] # rubocop:disable Security/MarshalLoad
|
|
1134
|
+
end
|
|
1135
|
+
end
|
|
1136
|
+
|
|
979
1137
|
def digest_config(config)
|
|
980
1138
|
canonical = Cache::Descriptor.canonicalize_value(config || {})
|
|
981
1139
|
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
|
# )
|
|
@@ -29,10 +29,9 @@ module Rigor
|
|
|
29
29
|
# class-level methods whose block argument runs as if it were
|
|
30
30
|
# an instance method of the receiver.
|
|
31
31
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# `Scope#self_type` for
|
|
35
|
-
# arrives in slice 1b.
|
|
32
|
+
# Engine wiring: `Inference::MacroBlockSelfType.narrow_self_type_for`
|
|
33
|
+
# (called from expression_typer.rb) consults registered entries
|
|
34
|
+
# and narrows `Scope#self_type` for matching block call sites.
|
|
36
35
|
#
|
|
37
36
|
# ## Fields
|
|
38
37
|
#
|
|
@@ -41,9 +40,10 @@ module Rigor
|
|
|
41
40
|
# for the entry to fire. For Sinatra modular-style this is
|
|
42
41
|
# `"Sinatra::Base"`; the substrate's class-context match
|
|
43
42
|
# accepts every subclass.
|
|
44
|
-
# - `
|
|
43
|
+
# - `method_names` — Array of Symbol method names. A call shape
|
|
45
44
|
# `<receiver_subclass>.get('/path') { ... }` matches when
|
|
46
|
-
# `:get` is in this list.
|
|
45
|
+
# `:get` is in this list. (Named `verbs:` before ADR-60 WD2
|
|
46
|
+
# normalised the macro value-object vocabulary.)
|
|
47
47
|
# - `self_type` — Symbol selecting the kind of `self`-binding
|
|
48
48
|
# the substrate applies inside the block. Slice 1a accepts
|
|
49
49
|
# only `:receiver_instance` (the block runs as an instance
|
|
@@ -53,22 +53,22 @@ module Rigor
|
|
|
53
53
|
# ## Ractor-shareability
|
|
54
54
|
#
|
|
55
55
|
# All fields are frozen at construction (ADR-15 Phase 1).
|
|
56
|
-
# `
|
|
57
|
-
# not leak into the value. `Ractor.shareable?` returns
|
|
58
|
-
# after `#initialize`.
|
|
56
|
+
# `method_names` is dup-frozen so the caller's mutable array
|
|
57
|
+
# does not leak into the value. `Ractor.shareable?` returns
|
|
58
|
+
# true after `#initialize`.
|
|
59
59
|
class BlockAsMethod
|
|
60
60
|
SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
|
|
61
61
|
VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
|
|
62
62
|
|
|
63
|
-
attr_reader :receiver_constraint, :
|
|
63
|
+
attr_reader :receiver_constraint, :method_names, :self_type
|
|
64
64
|
|
|
65
|
-
def initialize(receiver_constraint:,
|
|
65
|
+
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
|
|
66
66
|
validate_receiver_constraint!(receiver_constraint)
|
|
67
|
-
|
|
67
|
+
validate_method_names!(method_names)
|
|
68
68
|
validate_self_type!(self_type)
|
|
69
69
|
|
|
70
70
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
71
|
-
@
|
|
71
|
+
@method_names = method_names.map(&:to_sym).freeze
|
|
72
72
|
@self_type = self_type
|
|
73
73
|
freeze
|
|
74
74
|
end
|
|
@@ -76,7 +76,7 @@ module Rigor
|
|
|
76
76
|
def to_h
|
|
77
77
|
{
|
|
78
78
|
"receiver_constraint" => receiver_constraint,
|
|
79
|
-
"
|
|
79
|
+
"method_names" => method_names.map(&:to_s),
|
|
80
80
|
"self_type" => self_type.to_s
|
|
81
81
|
}
|
|
82
82
|
end
|
|
@@ -84,13 +84,13 @@ module Rigor
|
|
|
84
84
|
def ==(other)
|
|
85
85
|
other.is_a?(BlockAsMethod) &&
|
|
86
86
|
receiver_constraint == other.receiver_constraint &&
|
|
87
|
-
|
|
87
|
+
method_names == other.method_names &&
|
|
88
88
|
self_type == other.self_type
|
|
89
89
|
end
|
|
90
90
|
alias eql? ==
|
|
91
91
|
|
|
92
92
|
def hash
|
|
93
|
-
[receiver_constraint,
|
|
93
|
+
[receiver_constraint, method_names, self_type].hash
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
private
|
|
@@ -103,17 +103,17 @@ module Rigor
|
|
|
103
103
|
"got #{value.inspect}"
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def
|
|
107
|
-
unless
|
|
106
|
+
def validate_method_names!(method_names)
|
|
107
|
+
unless method_names.is_a?(Array) && !method_names.empty?
|
|
108
108
|
raise ArgumentError,
|
|
109
|
-
"Plugin::Macro::BlockAsMethod#
|
|
109
|
+
"Plugin::Macro::BlockAsMethod#method_names must be a non-empty Array, got #{method_names.inspect}"
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
method_names.each do |v|
|
|
113
113
|
next if v.is_a?(Symbol) || (v.is_a?(String) && !v.empty?)
|
|
114
114
|
|
|
115
115
|
raise ArgumentError,
|
|
116
|
-
"Plugin::Macro::BlockAsMethod#
|
|
116
|
+
"Plugin::Macro::BlockAsMethod#method_names entries must be Symbol/non-empty String, " \
|
|
117
117
|
"got #{v.inspect}"
|
|
118
118
|
end
|
|
119
119
|
end
|
|
@@ -68,13 +68,10 @@ module Rigor
|
|
|
68
68
|
# to a later slice — the `returns:` declarations cost
|
|
69
69
|
# nothing to write today and unlock precision then.
|
|
70
70
|
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# `SyntheticMethodIndex` the dispatcher consults; slice 2c
|
|
76
|
-
# authors `plugins/rigor-dry-struct/` and
|
|
77
|
-
# `plugins/rigor-dry-types/` as the worked consumers.
|
|
71
|
+
# Engine wiring: `Inference::SyntheticMethodScanner` (slice 2b,
|
|
72
|
+
# `synthetic_method_scanner.rb`) consumes `manifest.heredoc_templates`.
|
|
73
|
+
# Worked consumers: `plugins/rigor-dry-struct/` and
|
|
74
|
+
# `plugins/rigor-dry-types/` (slice 2c).
|
|
78
75
|
class HeredocTemplate
|
|
79
76
|
NAME_PLACEHOLDER = "\#{name}"
|
|
80
77
|
|
|
@@ -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
|
}
|
|
@@ -78,12 +78,9 @@ module Rigor
|
|
|
78
78
|
# facts (attr_reader / after_save / etc.); slice 3 emits
|
|
79
79
|
# only the module's plain instance methods.
|
|
80
80
|
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# scanner that walks Tier B call sites + the per-method
|
|
85
|
-
# explosion via `SyntheticMethodIndex`; slice 3c authors
|
|
86
|
-
# `plugins/rigor-devise/` model side as the worked consumer.
|
|
81
|
+
# Engine wiring: `Inference::SyntheticMethodScanner#collect_trait_registries`
|
|
82
|
+
# (slice 3b) walks Tier B call sites and explodes per-method
|
|
83
|
+
# entries. Worked consumer: `plugins/rigor-devise/` (slice 3c).
|
|
87
84
|
class TraitRegistry
|
|
88
85
|
REST_POSITION = :rest
|
|
89
86
|
|