rigortype 0.1.8 → 0.1.9
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 +186 -513
- data/lib/rigor/analysis/check_rules.rb +20 -0
- data/lib/rigor/cli/annotate_command.rb +224 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli.rb +62 -4
- data/lib/rigor/environment.rb +9 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/expression_typer.rb +165 -6
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +148 -1
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/statement_evaluator.rb +3 -1
- data/lib/rigor/plugin/base.rb +39 -0
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +73 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +66 -0
- data/lib/rigor/triage/catalogue.rb +2 -2
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- metadata +11 -1
|
@@ -13,6 +13,13 @@ require_relative "method_dispatcher/rbs_dispatch"
|
|
|
13
13
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
14
14
|
require_relative "method_dispatcher/block_folding"
|
|
15
15
|
require_relative "method_dispatcher/file_folding"
|
|
16
|
+
require_relative "method_dispatcher/shellwords_folding"
|
|
17
|
+
require_relative "method_dispatcher/math_folding"
|
|
18
|
+
require_relative "method_dispatcher/time_folding"
|
|
19
|
+
require_relative "method_dispatcher/regexp_folding"
|
|
20
|
+
require_relative "method_dispatcher/cgi_folding"
|
|
21
|
+
require_relative "method_dispatcher/uri_folding"
|
|
22
|
+
require_relative "method_dispatcher/set_folding"
|
|
16
23
|
require_relative "method_dispatcher/kernel_dispatch"
|
|
17
24
|
require_relative "method_dispatcher/method_folding"
|
|
18
25
|
|
|
@@ -643,7 +650,7 @@ module Rigor
|
|
|
643
650
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
644
651
|
LiteralStringFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
645
652
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
646
|
-
|
|
653
|
+
dispatch_stdlib_module_tiers(receiver_type, method_name, arg_types) ||
|
|
647
654
|
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
648
655
|
MethodFolding.try_forward(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
649
656
|
BlockFolding.try_fold(
|
|
@@ -651,6 +658,21 @@ module Rigor
|
|
|
651
658
|
)
|
|
652
659
|
end
|
|
653
660
|
|
|
661
|
+
# Stdlib module singleton-folding tiers: File, Shellwords, Math,
|
|
662
|
+
# Time, Regexp, CGI, URI, Set. Extracted from
|
|
663
|
+
# `dispatch_precise_tiers` to keep the parent method within the
|
|
664
|
+
# cyclomatic-complexity limit.
|
|
665
|
+
def dispatch_stdlib_module_tiers(receiver_type, method_name, arg_types)
|
|
666
|
+
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
667
|
+
ShellwordsFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
668
|
+
MathFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
669
|
+
TimeFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
670
|
+
RegexpFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
671
|
+
CGIFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
672
|
+
URIFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
673
|
+
SetFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
|
|
674
|
+
end
|
|
675
|
+
|
|
654
676
|
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node = nil)
|
|
655
677
|
return nil if environment.nil?
|
|
656
678
|
|
|
@@ -771,6 +793,18 @@ module Rigor
|
|
|
771
793
|
array_lift = array_new_lift(receiver_type.class_name, arg_types)
|
|
772
794
|
return array_lift if array_lift
|
|
773
795
|
|
|
796
|
+
range_lift = range_new_lift(receiver_type.class_name, arg_types)
|
|
797
|
+
return range_lift if range_lift
|
|
798
|
+
|
|
799
|
+
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
800
|
+
return set_lift if set_lift
|
|
801
|
+
|
|
802
|
+
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
803
|
+
return regexp_lift if regexp_lift
|
|
804
|
+
|
|
805
|
+
date_lift = date_new_lift(receiver_type.class_name, arg_types)
|
|
806
|
+
return date_lift if date_lift
|
|
807
|
+
|
|
774
808
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
775
809
|
end
|
|
776
810
|
|
|
@@ -840,6 +874,119 @@ module Rigor
|
|
|
840
874
|
type
|
|
841
875
|
end
|
|
842
876
|
|
|
877
|
+
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to
|
|
878
|
+
# `Constant[Range]` when both endpoints are `Constant[Integer]`
|
|
879
|
+
# or both are `Constant[String]`, and the optional third argument
|
|
880
|
+
# is a `Constant[true/false]`. Nil endpoints (beginless /
|
|
881
|
+
# endless ranges) are not folded because the useful instance
|
|
882
|
+
# methods (`to_a`, `first`, `last`) are not defined for them;
|
|
883
|
+
# the RBS tier answers `Nominal[Range]` for those forms.
|
|
884
|
+
def range_new_lift(class_name, arg_types)
|
|
885
|
+
return nil unless class_name == "Range"
|
|
886
|
+
return nil if arg_types.size < 2 || arg_types.size > 3
|
|
887
|
+
|
|
888
|
+
b_type = arg_types[0]
|
|
889
|
+
e_type = arg_types[1]
|
|
890
|
+
|
|
891
|
+
return nil unless b_type.is_a?(Type::Constant) && e_type.is_a?(Type::Constant)
|
|
892
|
+
|
|
893
|
+
b_val = b_type.value
|
|
894
|
+
e_val = e_type.value
|
|
895
|
+
|
|
896
|
+
# Only fold homogeneous Integer or String endpoint pairs.
|
|
897
|
+
return nil unless b_val.instance_of?(e_val.class)
|
|
898
|
+
return nil unless b_val.is_a?(Integer) || b_val.is_a?(String)
|
|
899
|
+
|
|
900
|
+
excl = range_new_excl(arg_types[2])
|
|
901
|
+
return nil if excl.nil?
|
|
902
|
+
|
|
903
|
+
Type::Combinator.constant_of(Range.new(b_val, e_val, excl))
|
|
904
|
+
rescue StandardError
|
|
905
|
+
nil
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# Resolves the optional `exclude_end` argument for `range_new_lift`.
|
|
909
|
+
# Returns `false` (no arg), `true`/`false` (Constant[bool] arg),
|
|
910
|
+
# or `nil` to signal "decline" (wrong type / wrong value class).
|
|
911
|
+
def range_new_excl(excl_type)
|
|
912
|
+
case excl_type
|
|
913
|
+
when nil then false
|
|
914
|
+
when Type::Constant
|
|
915
|
+
excl_type.value if [true, false].include?(excl_type.value)
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# `Set.new` / `Set.new(tuple_of_constants)` — folds to `Constant[Set]`
|
|
920
|
+
# when zero arguments are given or the single argument is a `Tuple`
|
|
921
|
+
# whose every element is a `Constant[T]`. Mirrors `SetFolding#fold_new`
|
|
922
|
+
# but lives here so the `:new` path in `try_meta_introspection` can
|
|
923
|
+
# reach it before the RBS tier answers `Nominal[Set]`.
|
|
924
|
+
def set_new_lift(class_name, arg_types)
|
|
925
|
+
return nil unless class_name == "Set"
|
|
926
|
+
return Type::Combinator.constant_of(::Set.new) if arg_types.empty?
|
|
927
|
+
return nil if arg_types.size > 1
|
|
928
|
+
|
|
929
|
+
arg = arg_types.first
|
|
930
|
+
return nil unless arg.is_a?(Type::Tuple)
|
|
931
|
+
return nil unless arg.elements.all?(Type::Constant)
|
|
932
|
+
|
|
933
|
+
values = arg.elements.map(&:value)
|
|
934
|
+
Type::Combinator.constant_of(::Set.new(values))
|
|
935
|
+
rescue StandardError
|
|
936
|
+
nil
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — folds to
|
|
940
|
+
# `Constant[Regexp]` when the pattern is a `Constant[String]`.
|
|
941
|
+
# Mirrors `RegexpFolding#fold_new` but lives here so the `:new` path
|
|
942
|
+
# in `try_meta_introspection` can reach it.
|
|
943
|
+
def regexp_new_lift(class_name, arg_types)
|
|
944
|
+
return nil unless class_name == "Regexp"
|
|
945
|
+
return nil if arg_types.empty? || arg_types.size > 2
|
|
946
|
+
|
|
947
|
+
pattern_arg = arg_types.first
|
|
948
|
+
return nil unless pattern_arg.is_a?(Type::Constant) && pattern_arg.value.is_a?(String)
|
|
949
|
+
|
|
950
|
+
opts = if arg_types.size == 2
|
|
951
|
+
opt_type = arg_types[1]
|
|
952
|
+
return nil unless opt_type.is_a?(Type::Constant)
|
|
953
|
+
|
|
954
|
+
opt_type.value
|
|
955
|
+
else
|
|
956
|
+
0
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
Type::Combinator.constant_of(Regexp.new(pattern_arg.value, opts))
|
|
960
|
+
rescue StandardError
|
|
961
|
+
nil
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
# `Date.new(y, m, d)` / `DateTime.new(y, m, d, h, min, s, off)`
|
|
965
|
+
# — folds to `Constant[Date]` / `Constant[DateTime]` when every
|
|
966
|
+
# argument is a `Constant` carrying an Integer (or, for the
|
|
967
|
+
# `DateTime` offset / `start` slots, a Rational or String).
|
|
968
|
+
# `Date.new` carries no timezone, and `DateTime.new`'s offset
|
|
969
|
+
# defaults to UTC, so the literal is machine-independent.
|
|
970
|
+
# `Date.today` / `Date.parse` are not constructors here — they
|
|
971
|
+
# are non-deterministic / string-dependent and stay
|
|
972
|
+
# `Nominal[Date]`.
|
|
973
|
+
DATE_NEW_CLASSES = %w[Date DateTime].freeze
|
|
974
|
+
private_constant :DATE_NEW_CLASSES
|
|
975
|
+
|
|
976
|
+
def date_new_lift(class_name, arg_types)
|
|
977
|
+
return nil unless DATE_NEW_CLASSES.include?(class_name)
|
|
978
|
+
return nil unless arg_types.size.between?(1, 8)
|
|
979
|
+
return nil unless arg_types.all?(Type::Constant)
|
|
980
|
+
|
|
981
|
+
values = arg_types.map(&:value)
|
|
982
|
+
return nil unless values.all? { |v| v.is_a?(Integer) || v.is_a?(Rational) || v.is_a?(String) }
|
|
983
|
+
|
|
984
|
+
klass = class_name == "Date" ? Date : DateTime
|
|
985
|
+
Type::Combinator.constant_of(klass.new(*values))
|
|
986
|
+
rescue StandardError
|
|
987
|
+
nil
|
|
988
|
+
end
|
|
989
|
+
|
|
843
990
|
CONSTANT_METACLASSES = {
|
|
844
991
|
Integer => "Integer", Float => "Float", String => "String",
|
|
845
992
|
Symbol => "Symbol", Range => "Range",
|
|
@@ -43,10 +43,15 @@ module Rigor
|
|
|
43
43
|
# method (either `def self.foo` or a `def foo` inside
|
|
44
44
|
# `class << self`); routes the lookup through
|
|
45
45
|
# `RbsLoader#singleton_method`.
|
|
46
|
-
|
|
46
|
+
# @param source_path [String, nil] the project-relative path of
|
|
47
|
+
# the file the method is defined in. Used to match ADR-28
|
|
48
|
+
# path-scoped protocol contracts; `nil` (the default for
|
|
49
|
+
# synthetic / probe scopes) disables the contract tier.
|
|
50
|
+
def initialize(environment:, class_path:, singleton:, source_path: nil)
|
|
47
51
|
@environment = environment
|
|
48
52
|
@class_path = class_path
|
|
49
53
|
@singleton = singleton
|
|
54
|
+
@source_path = source_path
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
# @param def_node [Prism::DefNode]
|
|
@@ -58,15 +63,22 @@ module Rigor
|
|
|
58
63
|
types = default_types_for(slots)
|
|
59
64
|
|
|
60
65
|
rbs_method = lookup_rbs_method(def_node)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
if rbs_method
|
|
67
|
+
apply_rbs_overloads(types, slots, rbs_method.method_types) unless rbs_method.method_types.empty?
|
|
68
|
+
# `rigor:v1:param: <name> <refinement>` annotations
|
|
69
|
+
# tighten the bound type for matching slots. Applied
|
|
70
|
+
# after the RBS-overload pass so the override is the
|
|
71
|
+
# authoritative answer regardless of what the RBS
|
|
72
|
+
# signature declared.
|
|
73
|
+
apply_param_overrides(types, slots, rbs_method)
|
|
74
|
+
end
|
|
75
|
+
# ADR-28 — a path-scoped protocol contract supplies the
|
|
76
|
+
# parameter type for a matching `def`. Applied last (most
|
|
77
|
+
# authoritative) and regardless of RBS presence: the
|
|
78
|
+
# methods a contract targets — controller actions and the
|
|
79
|
+
# like — typically have no RBS signature at all, so this
|
|
80
|
+
# tier must run even when `rbs_method` is nil.
|
|
81
|
+
apply_protocol_contract(types, slots, def_node)
|
|
70
82
|
types
|
|
71
83
|
end
|
|
72
84
|
|
|
@@ -75,6 +87,10 @@ module Rigor
|
|
|
75
87
|
ParamSlot = Data.define(:kind, :name, :index)
|
|
76
88
|
private_constant :ParamSlot
|
|
77
89
|
|
|
90
|
+
# Slot kinds a contract's positional `index` can address.
|
|
91
|
+
POSITIONAL_KINDS = %i[required_positional optional_positional].freeze
|
|
92
|
+
private_constant :POSITIONAL_KINDS
|
|
93
|
+
|
|
78
94
|
# Walk the Prism `ParametersNode` and emit one slot per named
|
|
79
95
|
# parameter, in declaration order. Anonymous slots (rest /
|
|
80
96
|
# keyword-rest with no name) are skipped because we have no
|
|
@@ -187,6 +203,47 @@ module Rigor
|
|
|
187
203
|
end
|
|
188
204
|
end
|
|
189
205
|
|
|
206
|
+
# ADR-28 — when a path-scoped protocol contract targets this
|
|
207
|
+
# `def` (file path matches the contract's `path_glob`, method
|
|
208
|
+
# name + singleton-ness match), replace each contracted
|
|
209
|
+
# positional slot's binding with the contract's declared type.
|
|
210
|
+
# The type name resolves against the environment lazily here;
|
|
211
|
+
# an unresolvable name (the protocol's RBS not loaded) falls
|
|
212
|
+
# through to whatever the prior tiers bound, fail-soft.
|
|
213
|
+
def apply_protocol_contract(types, slots, def_node)
|
|
214
|
+
return if @source_path.nil?
|
|
215
|
+
|
|
216
|
+
registry = @environment.respond_to?(:plugin_registry) ? @environment.plugin_registry : nil
|
|
217
|
+
return if registry.nil?
|
|
218
|
+
|
|
219
|
+
contract = matching_contract(registry, def_node)
|
|
220
|
+
return if contract.nil?
|
|
221
|
+
|
|
222
|
+
contract.param_types.each do |param_type|
|
|
223
|
+
slot = positional_slot_at(slots, param_type.index)
|
|
224
|
+
next if slot.nil? || slot.name.nil?
|
|
225
|
+
|
|
226
|
+
resolved = @environment.nominal_for_name(param_type.type_name)
|
|
227
|
+
next if resolved.nil?
|
|
228
|
+
|
|
229
|
+
types[slot.name] = resolved
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def matching_contract(registry, def_node)
|
|
234
|
+
contracts = registry.contracts_for_path(@source_path)
|
|
235
|
+
return nil if contracts.empty?
|
|
236
|
+
|
|
237
|
+
singleton = def_node.receiver.is_a?(Prism::SelfNode) || @singleton
|
|
238
|
+
contracts.find do |contract|
|
|
239
|
+
contract.method_name == def_node.name && contract.singleton == singleton
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def positional_slot_at(slots, index)
|
|
244
|
+
slots.find { |slot| POSITIONAL_KINDS.include?(slot.kind) && slot.index == index }
|
|
245
|
+
end
|
|
246
|
+
|
|
190
247
|
def collect_translated_types(method_types, slot)
|
|
191
248
|
rbs_types = method_types.flat_map do |mt|
|
|
192
249
|
t = rbs_type_for_slot(mt.type, slot)
|
|
@@ -386,8 +386,8 @@ module Rigor
|
|
|
386
386
|
analyse_local_read(node, scope)
|
|
387
387
|
when Prism::LocalVariableWriteNode
|
|
388
388
|
analyse_local_write(node, scope)
|
|
389
|
-
when Prism::InstanceVariableWriteNode
|
|
390
|
-
|
|
389
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
390
|
+
analyse_ivar(node, scope)
|
|
391
391
|
when Prism::ClassVariableWriteNode
|
|
392
392
|
analyse_cvar_write(node, scope)
|
|
393
393
|
when Prism::GlobalVariableWriteNode
|
|
@@ -767,7 +767,17 @@ module Rigor
|
|
|
767
767
|
]
|
|
768
768
|
end
|
|
769
769
|
|
|
770
|
-
|
|
770
|
+
# Truthy guard on an instance variable. Covers both the bare
|
|
771
|
+
# read — `if @ivar`, the left arm of `@ivar && @ivar.foo`,
|
|
772
|
+
# the receiver of `unless @ivar.nil?` once `!` reverses it —
|
|
773
|
+
# and the assignment-in-condition write `if @ivar = expr`.
|
|
774
|
+
# Mirrors `analyse_local_read` / `analyse_local_write`: the
|
|
775
|
+
# truthy edge narrows the ivar by `narrow_truthy` (dropping
|
|
776
|
+
# `nil` / `false`), the falsey edge by `narrow_falsey`. The
|
|
777
|
+
# narrowing is scoped to the guarded branch only, so it is
|
|
778
|
+
# purely additive — it never widens or re-narrows the ivar
|
|
779
|
+
# binding seen elsewhere.
|
|
780
|
+
def analyse_ivar(node, scope)
|
|
771
781
|
current = scope.ivar(node.name)
|
|
772
782
|
return nil if current.nil?
|
|
773
783
|
|
|
@@ -2024,15 +2034,24 @@ module Rigor
|
|
|
2024
2034
|
end
|
|
2025
2035
|
|
|
2026
2036
|
def analyse_nil_predicate(receiver, scope)
|
|
2027
|
-
|
|
2037
|
+
case receiver
|
|
2038
|
+
when Prism::LocalVariableReadNode
|
|
2039
|
+
current = scope.local(receiver.name)
|
|
2040
|
+
return nil if current.nil?
|
|
2028
2041
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2042
|
+
[
|
|
2043
|
+
scope.with_local(receiver.name, narrow_nil(current)),
|
|
2044
|
+
scope.with_local(receiver.name, narrow_non_nil(current))
|
|
2045
|
+
]
|
|
2046
|
+
when Prism::InstanceVariableReadNode
|
|
2047
|
+
current = scope.ivar(receiver.name)
|
|
2048
|
+
return nil if current.nil?
|
|
2031
2049
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2050
|
+
[
|
|
2051
|
+
scope.with_ivar(receiver.name, narrow_nil(current)),
|
|
2052
|
+
scope.with_ivar(receiver.name, narrow_non_nil(current))
|
|
2053
|
+
]
|
|
2054
|
+
end
|
|
2036
2055
|
end
|
|
2037
2056
|
|
|
2038
2057
|
# `a && b` short-circuits: the truthy edge is the truthy edge
|
|
@@ -1421,7 +1421,8 @@ module Rigor
|
|
|
1421
1421
|
binder = MethodParameterBinder.new(
|
|
1422
1422
|
environment: scope.environment,
|
|
1423
1423
|
class_path: current_class_path,
|
|
1424
|
-
singleton: singleton
|
|
1424
|
+
singleton: singleton,
|
|
1425
|
+
source_path: scope.source_path
|
|
1425
1426
|
)
|
|
1426
1427
|
bindings = binder.bind(def_node)
|
|
1427
1428
|
|
|
@@ -1486,6 +1487,7 @@ module Rigor
|
|
|
1486
1487
|
# remain reachable from inside nested bodies.
|
|
1487
1488
|
def build_fresh_body_scope # rubocop:disable Metrics/AbcSize
|
|
1488
1489
|
Scope.empty(environment: scope.environment)
|
|
1490
|
+
.with_source_path(scope.source_path)
|
|
1489
1491
|
.with_declared_types(scope.declared_types)
|
|
1490
1492
|
.with_discovered_classes(scope.discovered_classes)
|
|
1491
1493
|
.with_in_source_constants(scope.in_source_constants)
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -183,6 +183,45 @@ module Rigor
|
|
|
183
183
|
self.class.manifest
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
# ADR-25 — absolute RBS signature directories this plugin
|
|
187
|
+
# contributes. Resolves each `manifest.signature_paths` entry
|
|
188
|
+
# (declared relative to the plugin gem root) against that
|
|
189
|
+
# root. The gem root is the directory above `lib/` in the
|
|
190
|
+
# file that defined the plugin class (falling back to that
|
|
191
|
+
# file's directory for a non-conventional layout). Returns
|
|
192
|
+
# `[]` when the manifest declares no `signature_paths:` or
|
|
193
|
+
# the class is anonymous (an anonymous class cannot ship a
|
|
194
|
+
# gem). `Plugin::Loader` validates the resolved dirs exist at
|
|
195
|
+
# load time; `Environment.for_project` merges them into the
|
|
196
|
+
# signature-path set fed to `RbsLoader`.
|
|
197
|
+
def signature_paths
|
|
198
|
+
relative = manifest.signature_paths
|
|
199
|
+
return [] if relative.empty?
|
|
200
|
+
|
|
201
|
+
class_name = self.class.name
|
|
202
|
+
return [] if class_name.nil?
|
|
203
|
+
|
|
204
|
+
file, = Object.const_source_location(class_name)
|
|
205
|
+
return [] if file.nil?
|
|
206
|
+
|
|
207
|
+
before, separator, = file.rpartition("/lib/")
|
|
208
|
+
root = separator.empty? ? File.dirname(file) : before
|
|
209
|
+
relative.map { |rel| File.expand_path(rel, root) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# ADR-28 — the path-scoped method-protocol contracts this
|
|
213
|
+
# plugin contributes. Defaults to the manifest-declared
|
|
214
|
+
# `protocol_contracts:`; the same indirection
|
|
215
|
+
# `#signature_paths` uses, so a plugin MAY override this to
|
|
216
|
+
# fold per-project config into the contract set (e.g.
|
|
217
|
+
# substituting the convention `path_glob` with a user-supplied
|
|
218
|
+
# one) without the manifest having to be config-aware.
|
|
219
|
+
# `Plugin::Registry#protocol_contracts` aggregates the result
|
|
220
|
+
# across loaded plugins.
|
|
221
|
+
def protocol_contracts
|
|
222
|
+
manifest.protocol_contracts
|
|
223
|
+
end
|
|
224
|
+
|
|
186
225
|
# ADR-7 § "Slice 6-A/6-B" — per-plugin {IoBoundary}.
|
|
187
226
|
# Memoised so the boundary's accumulated `FileEntry`
|
|
188
227
|
# rows persist across producer invocations within the
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -125,7 +125,28 @@ module Rigor
|
|
|
125
125
|
seen_ids[manifest.id] = entry[:gem]
|
|
126
126
|
|
|
127
127
|
validate_config!(manifest, entry[:config])
|
|
128
|
-
instantiate(plugin_class, entry[:config])
|
|
128
|
+
plugin = instantiate(plugin_class, entry[:config])
|
|
129
|
+
validate_signature_paths!(plugin)
|
|
130
|
+
plugin
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ADR-25 — a plugin's manifest-declared `signature_paths:`
|
|
134
|
+
# are resolved (by `Plugin::Base#signature_paths`) against
|
|
135
|
+
# the plugin gem root. A declared directory that does not
|
|
136
|
+
# exist is a load-time failure for that plugin — loud, not
|
|
137
|
+
# silent, because a missing `sig/` means the bundle gem is
|
|
138
|
+
# broken. The raised LoadError is collected like any other
|
|
139
|
+
# load failure and the plugin drops from the registry.
|
|
140
|
+
def validate_signature_paths!(plugin)
|
|
141
|
+
plugin.signature_paths.each do |dir|
|
|
142
|
+
next if File.directory?(dir)
|
|
143
|
+
|
|
144
|
+
raise LoadError.new(
|
|
145
|
+
"plugin #{plugin.manifest.id.inspect} declares signature path #{dir.inspect} " \
|
|
146
|
+
"which is not a directory",
|
|
147
|
+
plugin_ref: plugin.manifest.id
|
|
148
|
+
)
|
|
149
|
+
end
|
|
129
150
|
end
|
|
130
151
|
|
|
131
152
|
def require_gem!(entry)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../inference/hkt_registry"
|
|
4
|
+
require_relative "protocol_contract"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Plugin
|
|
@@ -40,15 +41,16 @@ module Rigor
|
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
|
|
43
|
-
:owns_receivers, :
|
|
44
|
-
:trait_registries, :external_files, :hkt_registrations,
|
|
44
|
+
:owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
|
|
45
|
+
:heredoc_templates, :trait_registries, :external_files, :hkt_registrations,
|
|
46
|
+
:hkt_definitions, :signature_paths, :protocol_contracts
|
|
45
47
|
|
|
46
48
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
47
49
|
id:, version:,
|
|
48
50
|
description: nil, protocols: [], config_schema: {},
|
|
49
|
-
produces: [], consumes: [], owns_receivers: [], type_node_resolvers: [],
|
|
51
|
+
produces: [], consumes: [], owns_receivers: [], open_receivers: [], type_node_resolvers: [],
|
|
50
52
|
block_as_methods: [], heredoc_templates: [], trait_registries: [], external_files: [],
|
|
51
|
-
hkt_registrations: [], hkt_definitions: []
|
|
53
|
+
hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: []
|
|
52
54
|
)
|
|
53
55
|
validate_id!(id)
|
|
54
56
|
validate_version!(version)
|
|
@@ -56,6 +58,7 @@ module Rigor
|
|
|
56
58
|
validate_config_schema!(config_schema)
|
|
57
59
|
validate_produces!(produces)
|
|
58
60
|
validate_owns_receivers!(owns_receivers)
|
|
61
|
+
validate_open_receivers!(open_receivers)
|
|
59
62
|
validate_type_node_resolvers!(type_node_resolvers)
|
|
60
63
|
validate_block_as_methods!(block_as_methods)
|
|
61
64
|
validate_heredoc_templates!(heredoc_templates)
|
|
@@ -63,10 +66,12 @@ module Rigor
|
|
|
63
66
|
validate_external_files!(external_files)
|
|
64
67
|
validate_hkt_registrations!(hkt_registrations)
|
|
65
68
|
validate_hkt_definitions!(hkt_definitions)
|
|
69
|
+
validate_signature_paths!(signature_paths)
|
|
70
|
+
validate_protocol_contracts!(protocol_contracts)
|
|
66
71
|
|
|
67
72
|
assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
68
|
-
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
69
|
-
hkt_registrations, hkt_definitions)
|
|
73
|
+
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
74
|
+
external_files, hkt_registrations, hkt_definitions, signature_paths, protocol_contracts)
|
|
70
75
|
freeze
|
|
71
76
|
end
|
|
72
77
|
|
|
@@ -74,8 +79,8 @@ module Rigor
|
|
|
74
79
|
|
|
75
80
|
# rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
76
81
|
def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
77
|
-
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
78
|
-
hkt_registrations, hkt_definitions)
|
|
82
|
+
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
83
|
+
external_files, hkt_registrations, hkt_definitions, signature_paths, protocol_contracts)
|
|
79
84
|
@id = id.dup.freeze
|
|
80
85
|
@version = version.dup.freeze
|
|
81
86
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
@@ -84,6 +89,7 @@ module Rigor
|
|
|
84
89
|
@produces = produces.map(&:to_sym).freeze
|
|
85
90
|
@consumes = coerce_consumes(consumes)
|
|
86
91
|
@owns_receivers = owns_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
92
|
+
@open_receivers = open_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
87
93
|
@type_node_resolvers = type_node_resolvers.dup.freeze
|
|
88
94
|
@block_as_methods = block_as_methods.dup.freeze
|
|
89
95
|
@heredoc_templates = heredoc_templates.dup.freeze
|
|
@@ -91,6 +97,8 @@ module Rigor
|
|
|
91
97
|
@external_files = external_files.dup.freeze
|
|
92
98
|
@hkt_registrations = hkt_registrations.dup.freeze
|
|
93
99
|
@hkt_definitions = hkt_definitions.dup.freeze
|
|
100
|
+
@signature_paths = signature_paths.map { |p| p.to_s.dup.freeze }.freeze
|
|
101
|
+
@protocol_contracts = protocol_contracts.dup.freeze
|
|
94
102
|
end
|
|
95
103
|
# rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
|
|
96
104
|
|
|
@@ -119,7 +127,7 @@ module Rigor
|
|
|
119
127
|
errors
|
|
120
128
|
end
|
|
121
129
|
|
|
122
|
-
def to_h # rubocop:disable Metrics/AbcSize
|
|
130
|
+
def to_h # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
123
131
|
{
|
|
124
132
|
"id" => id,
|
|
125
133
|
"version" => version,
|
|
@@ -129,13 +137,16 @@ module Rigor
|
|
|
129
137
|
"produces" => produces.map(&:to_s),
|
|
130
138
|
"consumes" => consumes.map { |c| consumption_hash(c) },
|
|
131
139
|
"owns_receivers" => owns_receivers,
|
|
140
|
+
"open_receivers" => open_receivers,
|
|
132
141
|
"type_node_resolvers" => type_node_resolvers.map { |r| r.class.name },
|
|
133
142
|
"block_as_methods" => block_as_methods.map(&:to_h),
|
|
134
143
|
"heredoc_templates" => heredoc_templates.map(&:to_h),
|
|
135
144
|
"trait_registries" => trait_registries.map(&:to_h),
|
|
136
145
|
"external_files" => external_files.map(&:to_h),
|
|
137
146
|
"hkt_registrations" => hkt_registrations.map(&:to_h),
|
|
138
|
-
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } }
|
|
147
|
+
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } },
|
|
148
|
+
"signature_paths" => signature_paths,
|
|
149
|
+
"protocol_contracts" => protocol_contracts.map(&:to_h)
|
|
139
150
|
}
|
|
140
151
|
end
|
|
141
152
|
|
|
@@ -217,6 +228,24 @@ module Rigor
|
|
|
217
228
|
"got #{owns_receivers.inspect}"
|
|
218
229
|
end
|
|
219
230
|
|
|
231
|
+
# ADR-26 — `open_receivers:` declares the class names this
|
|
232
|
+
# plugin marks as "open": statically known to respond beyond
|
|
233
|
+
# their RBS-declared method surface (e.g. `ActiveRecord::Relation`,
|
|
234
|
+
# which delegates an unbounded set of user-defined scopes to
|
|
235
|
+
# its model). `Analysis::CheckRules` skips the
|
|
236
|
+
# `call.undefined-method` rule for a receiver whose class any
|
|
237
|
+
# loaded plugin lists here — flagging a method on a class
|
|
238
|
+
# with an open dynamic surface is unsound. Distinct from
|
|
239
|
+
# `owns_receivers:` (which routes dispatch); this one only
|
|
240
|
+
# suppresses the diagnostic.
|
|
241
|
+
def validate_open_receivers!(open_receivers)
|
|
242
|
+
return if open_receivers.is_a?(Array) && open_receivers.all? { |c| c.is_a?(String) && !c.empty? }
|
|
243
|
+
|
|
244
|
+
raise ArgumentError,
|
|
245
|
+
"plugin manifest open_receivers must be an Array of non-empty String, " \
|
|
246
|
+
"got #{open_receivers.inspect}"
|
|
247
|
+
end
|
|
248
|
+
|
|
220
249
|
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
221
250
|
# plugin-supplied `TypeNodeResolver` instances the parser
|
|
222
251
|
# consults (in slice 3) when an RBS::Extended payload's
|
|
@@ -326,6 +355,40 @@ module Rigor
|
|
|
326
355
|
"Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
|
|
327
356
|
end
|
|
328
357
|
|
|
358
|
+
# ADR-25 — `signature_paths:` declares the RBS signature
|
|
359
|
+
# directories this plugin gem ships, as paths relative to the
|
|
360
|
+
# plugin's own gem root (e.g. `["sig"]`). `Plugin::Base#signature_paths`
|
|
361
|
+
# resolves them to absolute dirs against the gem root; the
|
|
362
|
+
# loader validates each exists and `Environment.for_project`
|
|
363
|
+
# merges the resolved set into the RBS environment.
|
|
364
|
+
def validate_signature_paths!(paths)
|
|
365
|
+
return if paths.is_a?(Array) && paths.all? { |p| p.is_a?(String) && !p.empty? }
|
|
366
|
+
|
|
367
|
+
raise ArgumentError,
|
|
368
|
+
"plugin manifest signature_paths must be an Array of non-empty String, " \
|
|
369
|
+
"got #{paths.inspect}"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# ADR-28 — `protocol_contracts:` declares the path-scoped
|
|
373
|
+
# method-protocol contracts the plugin contributes. Each
|
|
374
|
+
# entry MUST be a `Rigor::Plugin::ProtocolContract`. The
|
|
375
|
+
# registry aggregator on `Plugin::Registry` flattens
|
|
376
|
+
# contracts across loaded plugins; the engine consults them
|
|
377
|
+
# in two places — `MethodParameterBinder` provides the
|
|
378
|
+
# declared parameter types into matching method bodies, and
|
|
379
|
+
# the contributing plugin's `#diagnostics_for_file` checks
|
|
380
|
+
# method presence + return-type conformance. The manifest
|
|
381
|
+
# field carries the plugin's *default* contracts; a plugin
|
|
382
|
+
# MAY override `Plugin::Base#protocol_contracts` to fold in
|
|
383
|
+
# per-project config (e.g. a custom convention path).
|
|
384
|
+
def validate_protocol_contracts!(entries)
|
|
385
|
+
return if entries.is_a?(Array) && entries.all?(ProtocolContract)
|
|
386
|
+
|
|
387
|
+
raise ArgumentError,
|
|
388
|
+
"plugin manifest protocol_contracts must be an Array of " \
|
|
389
|
+
"Rigor::Plugin::ProtocolContract instances, got #{entries.inspect}"
|
|
390
|
+
end
|
|
391
|
+
|
|
329
392
|
def coerce_consumes(consumes)
|
|
330
393
|
unless consumes.is_a?(Array)
|
|
331
394
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|