rigortype 0.1.2 → 0.1.4
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 +135 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
- data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +206 -6
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +59 -6
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +235 -0
- data/lib/rigor/configuration.rb +45 -11
- data/lib/rigor/environment.rb +47 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +7 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +233 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +70 -6
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +49 -7
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +6 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +58 -1
|
@@ -785,6 +785,7 @@ module Rigor
|
|
|
785
785
|
evaluate_block_if_present(node)
|
|
786
786
|
post_scope = record_closure_escape_if_any(node)
|
|
787
787
|
post_scope = apply_rbs_extended_assertions(node, post_scope)
|
|
788
|
+
post_scope = apply_plugin_assertions(node, post_scope)
|
|
788
789
|
post_scope = apply_rspec_matcher_narrowing(node, post_scope)
|
|
789
790
|
[call_type, post_scope]
|
|
790
791
|
end
|
|
@@ -888,7 +889,7 @@ module Rigor
|
|
|
888
889
|
# or nil otherwise. Centralised so each per-matcher
|
|
889
890
|
# decoder can short-circuit on a non-matching outer
|
|
890
891
|
# call.
|
|
891
|
-
def rspec_expectation_target(call_node)
|
|
892
|
+
def rspec_expectation_target(call_node)
|
|
892
893
|
receiver = call_node.receiver
|
|
893
894
|
return nil unless receiver.is_a?(Prism::CallNode) && receiver.name == :expect
|
|
894
895
|
return nil unless receiver.receiver.nil?
|
|
@@ -966,7 +967,7 @@ module Rigor
|
|
|
966
967
|
method_def = resolve_call_method(call_node, current_scope)
|
|
967
968
|
return current_scope if method_def.nil?
|
|
968
969
|
|
|
969
|
-
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
970
|
+
contribution = RbsExtended.read_flow_contribution(method_def, environment: current_scope.environment)
|
|
970
971
|
return current_scope if contribution.nil?
|
|
971
972
|
|
|
972
973
|
result = Rigor::FlowContribution::Merger.merge([contribution])
|
|
@@ -978,7 +979,62 @@ module Rigor
|
|
|
978
979
|
end
|
|
979
980
|
end
|
|
980
981
|
|
|
981
|
-
|
|
982
|
+
# ADR-7 § "Slice 4-A" / T.bind priority slice 2 — applies
|
|
983
|
+
# the post-return facts plugin contributions produce. This
|
|
984
|
+
# is the sibling of {apply_rbs_extended_assertions}: the
|
|
985
|
+
# carrier (`Rigor::FlowContribution::Fact`) and the
|
|
986
|
+
# downstream narrowing path (`apply_post_return_fact` →
|
|
987
|
+
# `apply_self_post_return_fact`) are the same; only the
|
|
988
|
+
# *source* of the bundle changes (RBS::Extended vs the
|
|
989
|
+
# registered plugins' `flow_contribution_for`).
|
|
990
|
+
#
|
|
991
|
+
# `:self`-targeted facts narrow `scope.self_type` for the
|
|
992
|
+
# surrounding scope. In a block body, the surrounding
|
|
993
|
+
# scope is the block's own scope, so the narrowing applies
|
|
994
|
+
# to the rest of the block — exactly the contract Sorbet's
|
|
995
|
+
# `T.bind(self, T)` commits to.
|
|
996
|
+
#
|
|
997
|
+
# `:parameter`-targeted facts only land when the called
|
|
998
|
+
# method has an authoritative RBS sig (via
|
|
999
|
+
# `resolve_call_method`); plugins recognising their own
|
|
1000
|
+
# synthetic call shapes (e.g. `T.assert_type!`) have no
|
|
1001
|
+
# method_def and the parameter facts silently skip — the
|
|
1002
|
+
# plugin's own diagnostics_for_file path covers those
|
|
1003
|
+
# cases. The full plugin-side parameter-targeting story
|
|
1004
|
+
# (PHPStan-style Type-Specifying Extensions on
|
|
1005
|
+
# plugin-recognised calls) lives behind a follow-up slice
|
|
1006
|
+
# that introduces `:local` / `:argument_at` target kinds.
|
|
1007
|
+
def apply_plugin_assertions(call_node, current_scope)
|
|
1008
|
+
registry = current_scope.environment&.plugin_registry
|
|
1009
|
+
return current_scope if registry.nil? || registry.empty?
|
|
1010
|
+
|
|
1011
|
+
contributions = collect_plugin_contributions(registry, call_node, current_scope)
|
|
1012
|
+
return current_scope if contributions.empty?
|
|
1013
|
+
|
|
1014
|
+
result = Rigor::FlowContribution::Merger.merge(contributions)
|
|
1015
|
+
post_return = result.post_return_facts
|
|
1016
|
+
return current_scope if post_return.empty?
|
|
1017
|
+
|
|
1018
|
+
method_def = resolve_call_method(call_node, current_scope)
|
|
1019
|
+
post_return.reduce(current_scope) do |scope_acc, fact|
|
|
1020
|
+
apply_post_return_fact(fact, call_node, scope_acc, method_def)
|
|
1021
|
+
end
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
# Walks the registry and collects each plugin's
|
|
1025
|
+
# `flow_contribution_for` result, swallowing per-plugin
|
|
1026
|
+
# exceptions so a buggy plugin can't abort the assertion
|
|
1027
|
+
# path. Mirrors `MethodDispatcher.collect_plugin_contributions`
|
|
1028
|
+
# exactly — the two paths consume the same hook.
|
|
1029
|
+
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
1030
|
+
registry.plugins.filter_map do |plugin|
|
|
1031
|
+
plugin.flow_contribution_for(call_node: call_node, scope: current_scope)
|
|
1032
|
+
rescue StandardError
|
|
1033
|
+
nil
|
|
1034
|
+
end
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def resolve_call_method(call_node, current_scope)
|
|
982
1038
|
receiver_node = call_node.receiver
|
|
983
1039
|
receiver_type =
|
|
984
1040
|
if receiver_node
|
|
@@ -1065,6 +1121,14 @@ module Rigor
|
|
|
1065
1121
|
end
|
|
1066
1122
|
|
|
1067
1123
|
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
1124
|
+
# Plugin-source contributions arrive without an
|
|
1125
|
+
# authoritative method_def (the plugin recognised the
|
|
1126
|
+
# call shape directly). Parameter-targeting falls back
|
|
1127
|
+
# to "no narrow" in that case — the wider plugin-side
|
|
1128
|
+
# parameter mapping (`:local` / `:argument_at`) is a
|
|
1129
|
+
# follow-up slice.
|
|
1130
|
+
return nil if method_def.nil?
|
|
1131
|
+
|
|
1068
1132
|
arguments = call_node.arguments&.arguments || []
|
|
1069
1133
|
method_def.method_types.each do |mt|
|
|
1070
1134
|
params = mt.type.required_positionals + mt.type.optional_positionals
|
|
@@ -1330,6 +1394,8 @@ module Rigor
|
|
|
1330
1394
|
.with_class_ivars(scope.class_ivars)
|
|
1331
1395
|
.with_class_cvars(scope.class_cvars)
|
|
1332
1396
|
.with_program_globals(scope.program_globals)
|
|
1397
|
+
.with_discovered_methods(scope.discovered_methods)
|
|
1398
|
+
.with_discovered_method_visibilities(scope.discovered_method_visibilities)
|
|
1333
1399
|
end
|
|
1334
1400
|
|
|
1335
1401
|
def singleton_def?(def_node)
|
|
@@ -1437,7 +1503,7 @@ module Rigor
|
|
|
1437
1503
|
EXIT_CALL_NAMES = %i[raise throw exit abort fail].freeze
|
|
1438
1504
|
private_constant :EXIT_CALL_NAMES
|
|
1439
1505
|
|
|
1440
|
-
def branch_unconditionally_exits?(node)
|
|
1506
|
+
def branch_unconditionally_exits?(node)
|
|
1441
1507
|
return false if node.nil?
|
|
1442
1508
|
|
|
1443
1509
|
case node
|
|
@@ -1543,7 +1609,6 @@ module Rigor
|
|
|
1543
1609
|
# Returns an array of `[Symbol, Rigor::Type]` pairs for every
|
|
1544
1610
|
# variable captured by `pattern`. Unrecognised pattern nodes
|
|
1545
1611
|
# contribute no bindings (fail-soft).
|
|
1546
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
1547
1612
|
def collect_in_pattern_bindings(subject, pattern, scope)
|
|
1548
1613
|
case pattern
|
|
1549
1614
|
when Prism::CapturePatternNode
|
|
@@ -1565,7 +1630,6 @@ module Rigor
|
|
|
1565
1630
|
[]
|
|
1566
1631
|
end
|
|
1567
1632
|
end
|
|
1568
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
1569
1633
|
|
|
1570
1634
|
def collect_array_pattern_bindings(pattern, scope)
|
|
1571
1635
|
bindings = [*pattern.requireds, *pattern.posts].flat_map do |elem|
|
|
@@ -137,7 +137,6 @@ module Rigor
|
|
|
137
137
|
# to the same `#get(url, timeout:, max_bytes:)` shape so the
|
|
138
138
|
# tests don't require network access.
|
|
139
139
|
class DefaultHttpClient
|
|
140
|
-
# rubocop:disable Metrics/MethodLength
|
|
141
140
|
def get(url, timeout:, max_bytes:)
|
|
142
141
|
require "net/http"
|
|
143
142
|
require "uri"
|
|
@@ -169,7 +168,6 @@ module Rigor
|
|
|
169
168
|
end
|
|
170
169
|
body
|
|
171
170
|
end
|
|
172
|
-
# rubocop:enable Metrics/MethodLength
|
|
173
171
|
end
|
|
174
172
|
end
|
|
175
173
|
end
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -81,7 +81,7 @@ module Rigor
|
|
|
81
81
|
# "rigor-rails"
|
|
82
82
|
# { "gem" => "rigor-rails", "id" => "rails", "config" => {...} }
|
|
83
83
|
# { gem: "rigor-rails", id: "rails", config: {...} }
|
|
84
|
-
def normalise_entry(raw, index)
|
|
84
|
+
def normalise_entry(raw, index)
|
|
85
85
|
case raw
|
|
86
86
|
when String
|
|
87
87
|
{ gem: raw, id: nil, config: {} }
|
|
@@ -136,7 +136,7 @@ module Rigor
|
|
|
136
136
|
)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
def lookup_plugin_class!(entry, newly_registered)
|
|
139
|
+
def lookup_plugin_class!(entry, newly_registered)
|
|
140
140
|
if entry[:id]
|
|
141
141
|
plugin_class = Plugin.registered_for(entry[:id])
|
|
142
142
|
unless plugin_class
|
|
@@ -11,7 +11,7 @@ module Rigor
|
|
|
11
11
|
# The fields are pinned by ADR-2 § "Registration, Configuration,
|
|
12
12
|
# and Caching"; the v0.1.0 plugin contract surface treats this
|
|
13
13
|
# struct as the public manifest shape.
|
|
14
|
-
class Manifest
|
|
14
|
+
class Manifest
|
|
15
15
|
# Same regex {Rigor::Cache::Store::VALID_PRODUCER_ID} uses,
|
|
16
16
|
# so plugin ids round-trip through cache producer ids and
|
|
17
17
|
# `plugin.<id>.<rule>` diagnostic identifiers without escape.
|
|
@@ -31,32 +31,38 @@ module Rigor
|
|
|
31
31
|
# topological sort + missing-producer detection (slice 5);
|
|
32
32
|
# slice 4 carries the declarations on the manifest but the
|
|
33
33
|
# loader does not yet enforce them.
|
|
34
|
-
Consumption
|
|
34
|
+
class Consumption < Data.define(:plugin_id, :name, :optional)
|
|
35
35
|
def initialize(plugin_id:, name:, optional: false)
|
|
36
36
|
super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes
|
|
40
|
+
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
|
|
41
|
+
:owns_receivers, :type_node_resolvers
|
|
41
42
|
|
|
42
43
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
43
44
|
id:, version:,
|
|
44
45
|
description: nil, protocols: [], config_schema: {},
|
|
45
|
-
produces: [], consumes: []
|
|
46
|
+
produces: [], consumes: [], owns_receivers: [], type_node_resolvers: []
|
|
46
47
|
)
|
|
47
48
|
validate_id!(id)
|
|
48
49
|
validate_version!(version)
|
|
49
50
|
validate_protocols!(protocols)
|
|
50
51
|
validate_config_schema!(config_schema)
|
|
51
52
|
validate_produces!(produces)
|
|
53
|
+
validate_owns_receivers!(owns_receivers)
|
|
54
|
+
validate_type_node_resolvers!(type_node_resolvers)
|
|
52
55
|
|
|
53
|
-
assign_fields(id, version, description, protocols, config_schema, produces, consumes
|
|
56
|
+
assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
57
|
+
type_node_resolvers)
|
|
54
58
|
freeze
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
private
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
# rubocop:disable Metrics/ParameterLists
|
|
64
|
+
def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
65
|
+
type_node_resolvers)
|
|
60
66
|
@id = id.dup.freeze
|
|
61
67
|
@version = version.dup.freeze
|
|
62
68
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
@@ -64,7 +70,10 @@ module Rigor
|
|
|
64
70
|
@config_schema = config_schema.to_h { |k, v| [k.to_s.dup.freeze, v.to_sym] }.freeze
|
|
65
71
|
@produces = produces.map(&:to_sym).freeze
|
|
66
72
|
@consumes = coerce_consumes(consumes)
|
|
73
|
+
@owns_receivers = owns_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
74
|
+
@type_node_resolvers = type_node_resolvers.dup.freeze
|
|
67
75
|
end
|
|
76
|
+
# rubocop:enable Metrics/ParameterLists
|
|
68
77
|
|
|
69
78
|
public
|
|
70
79
|
|
|
@@ -99,7 +108,9 @@ module Rigor
|
|
|
99
108
|
"protocols" => protocols.map(&:to_s),
|
|
100
109
|
"config_schema" => config_schema.to_h { |k, v| [k, v.to_s] },
|
|
101
110
|
"produces" => produces.map(&:to_s),
|
|
102
|
-
"consumes" => consumes.map { |c| consumption_hash(c) }
|
|
111
|
+
"consumes" => consumes.map { |c| consumption_hash(c) },
|
|
112
|
+
"owns_receivers" => owns_receivers,
|
|
113
|
+
"type_node_resolvers" => type_node_resolvers.map { |r| r.class.name }
|
|
103
114
|
}
|
|
104
115
|
end
|
|
105
116
|
|
|
@@ -166,6 +177,37 @@ module Rigor
|
|
|
166
177
|
raise ArgumentError, "plugin manifest produces must be an Array of Symbol/String, got #{produces.inspect}"
|
|
167
178
|
end
|
|
168
179
|
|
|
180
|
+
# ADR-10 5a — `owns_receivers:` declares the class names
|
|
181
|
+
# this plugin claims sole ownership of. The dispatcher's
|
|
182
|
+
# dependency-source-inference tier consults this list
|
|
183
|
+
# before consulting its own catalog: receivers owned by a
|
|
184
|
+
# registered plugin (directly or via subclass) decline,
|
|
185
|
+
# so plugin contributions stay authoritative for those
|
|
186
|
+
# types.
|
|
187
|
+
def validate_owns_receivers!(owns_receivers)
|
|
188
|
+
return if owns_receivers.is_a?(Array) && owns_receivers.all? { |c| c.is_a?(String) && !c.empty? }
|
|
189
|
+
|
|
190
|
+
raise ArgumentError,
|
|
191
|
+
"plugin manifest owns_receivers must be an Array of non-empty String, " \
|
|
192
|
+
"got #{owns_receivers.inspect}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
196
|
+
# plugin-supplied `TypeNodeResolver` instances the parser
|
|
197
|
+
# consults (in slice 3) when an RBS::Extended payload's
|
|
198
|
+
# named- or generic-type head misses the built-in registry.
|
|
199
|
+
# Slice 2 carries the declarations on the manifest and the
|
|
200
|
+
# registry exposes them in registration order; the parser
|
|
201
|
+
# integration that actually drives the chain lands in
|
|
202
|
+
# slice 3.
|
|
203
|
+
def validate_type_node_resolvers!(resolvers)
|
|
204
|
+
return if resolvers.is_a?(Array) && resolvers.all?(TypeNodeResolver)
|
|
205
|
+
|
|
206
|
+
raise ArgumentError,
|
|
207
|
+
"plugin manifest type_node_resolvers must be an Array of " \
|
|
208
|
+
"Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
|
|
209
|
+
end
|
|
210
|
+
|
|
169
211
|
def coerce_consumes(consumes)
|
|
170
212
|
unless consumes.is_a?(Array)
|
|
171
213
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
@@ -44,6 +44,17 @@ module Rigor
|
|
|
44
44
|
!load_errors.empty?
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# ADR-13 slice 2 — flat ordered list of every loaded
|
|
48
|
+
# plugin's manifest-declared {TypeNodeResolver} instances,
|
|
49
|
+
# in plugin registration order. Slice 3 wires this into
|
|
50
|
+
# the parser's resolver chain; until then the method is a
|
|
51
|
+
# read-side aggregator only. The first non-nil
|
|
52
|
+
# `#resolve(node, scope)` return wins per ADR-13 WD3 / WD5
|
|
53
|
+
# — registration order is the user's lever.
|
|
54
|
+
def type_node_resolvers
|
|
55
|
+
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
EMPTY = new.freeze
|
|
48
59
|
end
|
|
49
60
|
end
|
|
@@ -42,7 +42,7 @@ module Rigor
|
|
|
42
42
|
class Services
|
|
43
43
|
attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy, :fact_store
|
|
44
44
|
|
|
45
|
-
def initialize(
|
|
45
|
+
def initialize(
|
|
46
46
|
reflection:, type:, configuration:,
|
|
47
47
|
cache_store: nil, trust_policy: nil, fact_store: nil
|
|
48
48
|
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# Plugin-supplied resolver for custom named / generic type
|
|
6
|
+
# vocabulary in RBS::Extended payloads. ADR-13 § "Decision".
|
|
7
|
+
#
|
|
8
|
+
# Subclasses override {#resolve} to return a
|
|
9
|
+
# {Rigor::Type::Base} when the node matches the vocabulary
|
|
10
|
+
# the resolver covers, or `nil` to fall through to the next
|
|
11
|
+
# resolver in the chain (and finally to the built-in / RBS
|
|
12
|
+
# fallback). The base implementation returns `nil` so an
|
|
13
|
+
# unimplemented subclass is a safe no-op.
|
|
14
|
+
#
|
|
15
|
+
# Resolvers are registered through their plugin's manifest
|
|
16
|
+
# under the `type_node_resolvers:` slot:
|
|
17
|
+
#
|
|
18
|
+
# class RigorTypescriptUtilityTypes < Rigor::Plugin::Base
|
|
19
|
+
# manifest(
|
|
20
|
+
# id: "typescript-utility-types",
|
|
21
|
+
# version: "0.1.0",
|
|
22
|
+
# type_node_resolvers: [Resolvers::Pick.new,
|
|
23
|
+
# Resolvers::Omit.new]
|
|
24
|
+
# )
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# Slice 2 of the ADR-13 envelope (this file) ships the base
|
|
28
|
+
# class + manifest hook + registry aggregation. The parser-
|
|
29
|
+
# side wiring that actually consults the resolver chain
|
|
30
|
+
# arrives in slice 3, when {Rigor::TypeNode::NameScope} and
|
|
31
|
+
# the dispatcher between {Rigor::Builtins::ImportedRefinements::Parser}
|
|
32
|
+
# and the chain land. Until then resolvers can be unit-tested
|
|
33
|
+
# in isolation but never run for a real `%a{rigor:v1:...}`
|
|
34
|
+
# payload.
|
|
35
|
+
#
|
|
36
|
+
# Resolvers SHOULD be stateless and re-entrant; the registry
|
|
37
|
+
# builds the chain once per `Analysis::Runner.run` and may
|
|
38
|
+
# consult any resolver multiple times for the same node.
|
|
39
|
+
class TypeNodeResolver
|
|
40
|
+
# @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic]
|
|
41
|
+
# the parser-emitted node the chain is asking about.
|
|
42
|
+
# @param scope [Rigor::TypeNode::NameScope] companion
|
|
43
|
+
# value object (slice 3); slice 2 invocations MAY pass
|
|
44
|
+
# `nil` because the chain doesn't exist yet.
|
|
45
|
+
# @return [Rigor::Type::Base, nil] resolved type, or `nil`
|
|
46
|
+
# to fall through.
|
|
47
|
+
def resolve(node, scope) # rubocop:disable Lint/UnusedMethodArgument
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module RbsExtended
|
|
5
|
+
# ADR-13 slice 3b — per-run accumulator for `RBS::Extended`
|
|
6
|
+
# diagnostic events that the parser / resolver cannot surface
|
|
7
|
+
# at the point of failure (the parsers are fail-soft, returning
|
|
8
|
+
# `nil` so call sites fall back to the RBS-declared type).
|
|
9
|
+
#
|
|
10
|
+
# Owns two event streams:
|
|
11
|
+
#
|
|
12
|
+
# - `#unresolved_payloads` — `rigor:v1:*` directive payloads
|
|
13
|
+
# the resolver could not turn into a {Rigor::Type}. Surface
|
|
14
|
+
# as `dynamic.rbs-extended.unresolved` `:info` diagnostics.
|
|
15
|
+
# - `#lossy_projections` — shape-projection type functions
|
|
16
|
+
# (`pick_of` / `omit_of` / `partial_of` / `required_of` /
|
|
17
|
+
# `readonly_of`) applied to a carrier that does not preserve
|
|
18
|
+
# shape information (anything other than `Type::HashShape`
|
|
19
|
+
# / `Type::Tuple`). Surface as
|
|
20
|
+
# `dynamic.shape.lossy-projection` `:info` diagnostics.
|
|
21
|
+
#
|
|
22
|
+
# Mutable through the run; consumed once by
|
|
23
|
+
# {Rigor::Analysis::Runner} at end-of-run. Each event is
|
|
24
|
+
# deduplicated by `(payload, source_location)` for unresolved
|
|
25
|
+
# and `(head, source_location)` for lossy-projection so a
|
|
26
|
+
# single annotation read from many call sites yields one
|
|
27
|
+
# diagnostic.
|
|
28
|
+
#
|
|
29
|
+
# The reporter is intentionally thread-safe via a coarse
|
|
30
|
+
# `Mutex` because the inference engine may read the same
|
|
31
|
+
# method definition from multiple files in parallel; the
|
|
32
|
+
# critical sections are short (Array#include? + Array#<<) so
|
|
33
|
+
# the lock contention is negligible.
|
|
34
|
+
class Reporter
|
|
35
|
+
UnresolvedEntry = Data.define(:payload, :source_location)
|
|
36
|
+
LossyProjectionEntry = Data.define(:head, :source_location)
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@unresolved_payloads = []
|
|
40
|
+
@lossy_projections = []
|
|
41
|
+
@mutex = Mutex.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Array<UnresolvedEntry>] frozen snapshot of the
|
|
45
|
+
# accumulated unresolved-payload events.
|
|
46
|
+
def unresolved_payloads
|
|
47
|
+
@mutex.synchronize { @unresolved_payloads.dup.freeze }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array<LossyProjectionEntry>] frozen snapshot of
|
|
51
|
+
# the accumulated lossy-projection events.
|
|
52
|
+
def lossy_projections
|
|
53
|
+
@mutex.synchronize { @lossy_projections.dup.freeze }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Records a `dynamic.rbs-extended.unresolved` event. The
|
|
57
|
+
# `source_location` argument is the {RBS::Location} attached
|
|
58
|
+
# to the source annotation (or `nil` when the caller doesn't
|
|
59
|
+
# have one — the diagnostic falls back to a generic
|
|
60
|
+
# location in that case).
|
|
61
|
+
def record_unresolved(payload:, source_location: nil)
|
|
62
|
+
entry = UnresolvedEntry.new(payload: payload.to_s, source_location: source_location)
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
return if @unresolved_payloads.include?(entry)
|
|
65
|
+
|
|
66
|
+
@unresolved_payloads << entry
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Records a `dynamic.shape.lossy-projection` event for one
|
|
71
|
+
# of the five shape-projection heads. `head` MUST be a
|
|
72
|
+
# String (`"pick_of"`, `"omit_of"`, …); the diagnostic
|
|
73
|
+
# message identifies which projection degraded.
|
|
74
|
+
def record_lossy_projection(head:, source_location: nil)
|
|
75
|
+
entry = LossyProjectionEntry.new(head: head.to_s, source_location: source_location)
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
return if @lossy_projections.include?(entry)
|
|
78
|
+
|
|
79
|
+
@lossy_projections << entry
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# True when no events have accumulated. Used by callers
|
|
84
|
+
# that want to skip the diagnostic-emission pass entirely
|
|
85
|
+
# on the common no-event path.
|
|
86
|
+
def empty?
|
|
87
|
+
@mutex.synchronize { @unresolved_payloads.empty? && @lossy_projections.empty? }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|