rigortype 0.1.7 → 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 +23 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -3
- data/lib/rigor/analysis/runner.rb +178 -3
- data/lib/rigor/analysis/worker_session.rb +14 -3
- 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/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +71 -5
- 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 +300 -18
- 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/rbs_dispatch.rb +33 -8
- 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 +179 -4
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/scope_indexer.rb +156 -6
- data/lib/rigor/inference/statement_evaluator.rb +43 -21
- 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/scope.rb +46 -0
- data/lib/rigor/triage/catalogue.rb +296 -0
- data/lib/rigor/triage/hint.rb +27 -0
- data/lib/rigor/triage.rb +89 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +16 -1
|
@@ -109,12 +109,11 @@ module Rigor
|
|
|
109
109
|
discovered_methods = build_discovered_methods(root)
|
|
110
110
|
seeded_scope = seeded_scope.with_discovered_methods(discovered_methods)
|
|
111
111
|
|
|
112
|
-
# v0.0.2 #5
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
seeded_scope = seeded_scope.with_discovered_def_nodes(discovered_def_nodes)
|
|
112
|
+
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
113
|
+
# def nodes, the class -> superclass map, and the
|
|
114
|
+
# class/module -> included-modules map, each merged under
|
|
115
|
+
# the cross-file pre-pass seed (see below).
|
|
116
|
+
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
118
117
|
|
|
119
118
|
# v0.1.2 — per-class table of method visibilities
|
|
120
119
|
# (`:public` / `:private` / `:protected`). The
|
|
@@ -134,6 +133,31 @@ module Rigor
|
|
|
134
133
|
table
|
|
135
134
|
end
|
|
136
135
|
|
|
136
|
+
# v0.0.2 #5 + ADR-24 slice 2 — seeds the three
|
|
137
|
+
# project-method indexes onto `seeded_scope`: the
|
|
138
|
+
# per-instance-method def-node table, the class ->
|
|
139
|
+
# superclass map, and the class/module -> included-modules
|
|
140
|
+
# map. Each per-file table is merged UNDER the cross-file
|
|
141
|
+
# `discovered_def_index_for_paths` seed carried on
|
|
142
|
+
# `default_scope` — same-file declarations win per entry,
|
|
143
|
+
# the cross-file seed supplies sibling-file ancestors.
|
|
144
|
+
def merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
145
|
+
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
146
|
+
build_discovered_def_nodes(root)
|
|
147
|
+
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
148
|
+
superclasses = default_scope.discovered_superclasses.merge(
|
|
149
|
+
build_discovered_superclasses(root)
|
|
150
|
+
)
|
|
151
|
+
includes = default_scope.discovered_includes.merge(
|
|
152
|
+
build_discovered_includes(root)
|
|
153
|
+
) { |_class, cross_file, per_file| (cross_file + per_file).uniq }
|
|
154
|
+
|
|
155
|
+
seeded_scope
|
|
156
|
+
.with_discovered_def_nodes(def_nodes)
|
|
157
|
+
.with_discovered_superclasses(superclasses)
|
|
158
|
+
.with_discovered_includes(includes)
|
|
159
|
+
end
|
|
160
|
+
|
|
137
161
|
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
138
162
|
# by walking every `Prism::ClassNode` / `Prism::ModuleNode`
|
|
139
163
|
# body, descending into each nested `Prism::DefNode`, and
|
|
@@ -580,6 +604,94 @@ module Rigor
|
|
|
580
604
|
accumulator[class_name][def_node.name] = def_node
|
|
581
605
|
end
|
|
582
606
|
|
|
607
|
+
# ADR-24 slice 2 — per-class table mapping a fully
|
|
608
|
+
# qualified user class to its superclass name AS WRITTEN
|
|
609
|
+
# at the `class Foo < Bar` declaration. Only constant
|
|
610
|
+
# superclasses are recorded (`class Foo < Struct.new(...)`
|
|
611
|
+
# and other non-constant superclasses produce no entry).
|
|
612
|
+
# The as-written name is resolved to a qualified class at
|
|
613
|
+
# the call site against the subclass's lexical nesting —
|
|
614
|
+
# see `ExpressionTyper#resolve_ancestor_class_name`.
|
|
615
|
+
def build_discovered_superclasses(root)
|
|
616
|
+
accumulator = {}
|
|
617
|
+
walk_class_superclasses(root, [], accumulator)
|
|
618
|
+
accumulator.freeze
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def walk_class_superclasses(node, qualified_prefix, accumulator)
|
|
622
|
+
return unless node.is_a?(Prism::Node)
|
|
623
|
+
|
|
624
|
+
case node
|
|
625
|
+
when Prism::ClassNode
|
|
626
|
+
name = qualified_name_for(node.constant_path)
|
|
627
|
+
if name
|
|
628
|
+
full = (qualified_prefix + [name]).join("::")
|
|
629
|
+
superclass = node.superclass && qualified_name_for(node.superclass)
|
|
630
|
+
accumulator[full] = superclass if superclass
|
|
631
|
+
walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
632
|
+
return
|
|
633
|
+
end
|
|
634
|
+
when Prism::ModuleNode
|
|
635
|
+
name = qualified_name_for(node.constant_path)
|
|
636
|
+
if name
|
|
637
|
+
walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
638
|
+
return
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
node.compact_child_nodes.each do |child|
|
|
643
|
+
walk_class_superclasses(child, qualified_prefix, accumulator)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
MIXIN_CALL_NAMES = %i[include prepend].freeze
|
|
648
|
+
|
|
649
|
+
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
650
|
+
# qualified user class or module to the list of module
|
|
651
|
+
# names it `include`s / `prepend`s, AS WRITTEN at the
|
|
652
|
+
# mixin call (`include Foo` / `include Foo::Bar`). Only
|
|
653
|
+
# constant arguments are recorded; dynamic mixins
|
|
654
|
+
# (`include some_method`) produce no entry. `prepend` is
|
|
655
|
+
# bucketed with `include` — both contribute instance
|
|
656
|
+
# methods to the ancestor chain. `extend` is NOT tracked
|
|
657
|
+
# (it adds singleton methods; ADR-24 slice 2 resolves the
|
|
658
|
+
# instance-side chain).
|
|
659
|
+
def build_discovered_includes(root)
|
|
660
|
+
accumulator = {}
|
|
661
|
+
walk_class_includes(root, [], nil, accumulator)
|
|
662
|
+
accumulator.transform_values { |mods| mods.uniq.freeze }.freeze
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def walk_class_includes(node, qualified_prefix, current_class, accumulator)
|
|
666
|
+
return unless node.is_a?(Prism::Node)
|
|
667
|
+
|
|
668
|
+
case node
|
|
669
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
670
|
+
name = qualified_name_for(node.constant_path)
|
|
671
|
+
if name
|
|
672
|
+
full = (qualified_prefix + [name]).join("::")
|
|
673
|
+
walk_class_includes(node.body, qualified_prefix + [name], full, accumulator) if node.body
|
|
674
|
+
return
|
|
675
|
+
end
|
|
676
|
+
when Prism::CallNode
|
|
677
|
+
record_mixin_call(node, current_class, accumulator)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
node.compact_child_nodes.each do |child|
|
|
681
|
+
walk_class_includes(child, qualified_prefix, current_class, accumulator)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def record_mixin_call(node, current_class, accumulator)
|
|
686
|
+
return unless current_class && node.receiver.nil?
|
|
687
|
+
return unless MIXIN_CALL_NAMES.include?(node.name)
|
|
688
|
+
|
|
689
|
+
node.arguments&.arguments&.each do |arg|
|
|
690
|
+
mod = qualified_name_for(arg)
|
|
691
|
+
(accumulator[current_class] ||= []) << mod if mod
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
583
695
|
VISIBILITY_MODIFIERS = %i[public private protected].freeze
|
|
584
696
|
|
|
585
697
|
# v0.1.2 — per-class method-visibility table for the
|
|
@@ -845,6 +957,44 @@ module Rigor
|
|
|
845
957
|
accumulator.freeze
|
|
846
958
|
end
|
|
847
959
|
|
|
960
|
+
# ADR-24 slice 2 — cross-file companion to
|
|
961
|
+
# `discovered_classes_for_paths`. Walks every project
|
|
962
|
+
# file once and returns both the merged
|
|
963
|
+
# `discovered_def_nodes` table (a class reopened across
|
|
964
|
+
# files has its method tables merged) and the merged
|
|
965
|
+
# class -> superclass-name map. The engine consults these
|
|
966
|
+
# so an implicit-self call inside a subclass resolves
|
|
967
|
+
# against a superclass `def` declared in a sibling file
|
|
968
|
+
# (`Mastodon::CLI::Accounts` calling a helper defined in
|
|
969
|
+
# `Mastodon::CLI::Base`).
|
|
970
|
+
#
|
|
971
|
+
# @param paths [Array<String>] project file paths.
|
|
972
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
973
|
+
# @return [Hash{Symbol => Hash}] `{ def_nodes:, superclasses: }`
|
|
974
|
+
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
975
|
+
def_nodes = {}
|
|
976
|
+
superclasses = {}
|
|
977
|
+
includes = {}
|
|
978
|
+
paths.each do |path|
|
|
979
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
980
|
+
root = Prism.parse(File.read(physical), filepath: path).value
|
|
981
|
+
build_discovered_def_nodes(root).each do |class_name, methods|
|
|
982
|
+
(def_nodes[class_name] ||= {}).merge!(methods)
|
|
983
|
+
end
|
|
984
|
+
superclasses.merge!(build_discovered_superclasses(root))
|
|
985
|
+
build_discovered_includes(root).each do |class_name, mods|
|
|
986
|
+
includes[class_name] = ((includes[class_name] || []) + mods).uniq
|
|
987
|
+
end
|
|
988
|
+
rescue StandardError
|
|
989
|
+
# Skip files that fail to parse or read; the per-file
|
|
990
|
+
# analyzer surfaces the parse error separately.
|
|
991
|
+
next
|
|
992
|
+
end
|
|
993
|
+
def_nodes.each_value(&:freeze)
|
|
994
|
+
includes.each_value(&:freeze)
|
|
995
|
+
{ def_nodes: def_nodes.freeze, superclasses: superclasses.freeze, includes: includes.freeze }
|
|
996
|
+
end
|
|
997
|
+
|
|
848
998
|
# Class-only variant of `record_declarations` — descends
|
|
849
999
|
# into nested module bodies (so `module Foo; class Bar`
|
|
850
1000
|
# registers `Foo::Bar`) but never registers the module
|
|
@@ -358,9 +358,9 @@ module Rigor
|
|
|
358
358
|
# is the falsey edge of the predicate (subsequent
|
|
359
359
|
# statements observe the predicate-was-false world).
|
|
360
360
|
return [Type::Combinator.union(then_type, else_type), falsey_scope] \
|
|
361
|
-
if
|
|
361
|
+
if branch_terminates?(node.statements, then_type) && node.subsequent.nil?
|
|
362
362
|
return [Type::Combinator.union(then_type, else_type), truthy_scope] \
|
|
363
|
-
if
|
|
363
|
+
if branch_terminates?(node.subsequent, else_type) && node.statements
|
|
364
364
|
|
|
365
365
|
[
|
|
366
366
|
Type::Combinator.union(then_type, else_type),
|
|
@@ -385,9 +385,9 @@ module Rigor
|
|
|
385
385
|
# `if`: when the body unconditionally exits and there
|
|
386
386
|
# is no else, the post-scope is the truthy edge.
|
|
387
387
|
return [Type::Combinator.union(then_type, else_type), truthy_scope] \
|
|
388
|
-
if
|
|
388
|
+
if branch_terminates?(node.statements, then_type) && node.else_clause.nil?
|
|
389
389
|
return [Type::Combinator.union(then_type, else_type), falsey_scope] \
|
|
390
|
-
if
|
|
390
|
+
if branch_terminates?(node.else_clause, else_type) && node.statements
|
|
391
391
|
|
|
392
392
|
[
|
|
393
393
|
Type::Combinator.union(then_type, else_type),
|
|
@@ -709,24 +709,25 @@ module Rigor
|
|
|
709
709
|
# `a` when the RHS is skipped, while `a || b` can only produce
|
|
710
710
|
# the truthy fragment of `a` when the RHS is skipped.
|
|
711
711
|
#
|
|
712
|
-
# When the RHS
|
|
713
|
-
# `
|
|
714
|
-
#
|
|
715
|
-
# `a or
|
|
716
|
-
#
|
|
717
|
-
#
|
|
718
|
-
#
|
|
719
|
-
#
|
|
712
|
+
# When the RHS is a terminating branch — it `raise`s /
|
|
713
|
+
# `return`s / `throw`s / `exit`s / `break`s / `next`s, OR its
|
|
714
|
+
# inferred type is `Bot` (ADR-24 WD6: a divergent helper such
|
|
715
|
+
# as `a or fail_with_message(...)`, recognised via
|
|
716
|
+
# `branch_terminates?`) — the post-OR / post-AND scope is the
|
|
717
|
+
# LHS-skipped edge alone: `a or raise` only survives when `a`
|
|
718
|
+
# was truthy, so subsequent statements observe `a` narrowed to
|
|
719
|
+
# its truthy fragment; the symmetric `a and raise` survives
|
|
720
|
+
# only when `a` was falsey. Same shape as the `eval_if` /
|
|
721
|
+
# `eval_unless` early-return narrowing.
|
|
720
722
|
def eval_and_or(node)
|
|
721
723
|
left_type, left_scope = sub_eval(node.left, scope)
|
|
722
724
|
truthy_left, falsey_left = Narrowing.predicate_scopes(node.left, left_scope)
|
|
723
725
|
rhs_entry = node.is_a?(Prism::AndNode) ? truthy_left : falsey_left
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
#
|
|
728
|
-
#
|
|
729
|
-
sub_eval(node.right, rhs_entry)
|
|
726
|
+
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
727
|
+
|
|
728
|
+
if branch_terminates?(node.right, right_type)
|
|
729
|
+
# Control never reaches any statement after `a or raise`
|
|
730
|
+
# via the RHS edge — the RHS scope is discarded.
|
|
730
731
|
surviving_type =
|
|
731
732
|
if node.is_a?(Prism::AndNode)
|
|
732
733
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -737,7 +738,6 @@ module Rigor
|
|
|
737
738
|
return [surviving_type, surviving_scope]
|
|
738
739
|
end
|
|
739
740
|
|
|
740
|
-
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
741
741
|
skipped_type =
|
|
742
742
|
if node.is_a?(Prism::AndNode)
|
|
743
743
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -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
|
|
|
@@ -1484,8 +1485,9 @@ module Rigor
|
|
|
1484
1485
|
# ScopeIndexer-populated declaration overrides
|
|
1485
1486
|
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
|
|
1486
1487
|
# remain reachable from inside nested bodies.
|
|
1487
|
-
def build_fresh_body_scope
|
|
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)
|
|
@@ -1493,6 +1495,9 @@ module Rigor
|
|
|
1493
1495
|
.with_class_cvars(scope.class_cvars)
|
|
1494
1496
|
.with_program_globals(scope.program_globals)
|
|
1495
1497
|
.with_discovered_methods(scope.discovered_methods)
|
|
1498
|
+
.with_discovered_def_nodes(scope.discovered_def_nodes)
|
|
1499
|
+
.with_discovered_superclasses(scope.discovered_superclasses)
|
|
1500
|
+
.with_discovered_includes(scope.discovered_includes)
|
|
1496
1501
|
.with_discovered_method_visibilities(scope.discovered_method_visibilities)
|
|
1497
1502
|
end
|
|
1498
1503
|
|
|
@@ -1692,6 +1697,23 @@ module Rigor
|
|
|
1692
1697
|
end
|
|
1693
1698
|
end
|
|
1694
1699
|
|
|
1700
|
+
# ADR-24 WD6 / slice 3 — generalised terminating-branch
|
|
1701
|
+
# detection. `branch_unconditionally_exits?` recognises a
|
|
1702
|
+
# branch SYNTACTICALLY (return / next / break / a call
|
|
1703
|
+
# named raise / throw / exit / abort / fail). A branch
|
|
1704
|
+
# whose *inferred type is `Bot`* also terminates — it
|
|
1705
|
+
# cannot produce a value, so control never falls through
|
|
1706
|
+
# it — regardless of how it is spelled. The canonical
|
|
1707
|
+
# case is a resolved guard helper (`fail_with_message(...)`)
|
|
1708
|
+
# whose body always raises: ADR-24 slice 1 types the call
|
|
1709
|
+
# `bot`, and this OR-test makes `helper(...) if x.nil?`
|
|
1710
|
+
# narrow exactly like `raise ... if x.nil?`. The branch
|
|
1711
|
+
# type is already computed by `eval_if` / `eval_unless`.
|
|
1712
|
+
def branch_terminates?(branch_node, branch_type)
|
|
1713
|
+
branch_unconditionally_exits?(branch_node) ||
|
|
1714
|
+
branch_type.is_a?(Type::Bot)
|
|
1715
|
+
end
|
|
1716
|
+
|
|
1695
1717
|
def eval_branch_or_nil(branch_node, branch_scope)
|
|
1696
1718
|
return [Type::Combinator.constant_of(nil), branch_scope] if branch_node.nil?
|
|
1697
1719
|
|
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}"
|