rigortype 0.1.5 → 0.1.7
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 +76 -79
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +68 -3
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +142 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +142 -13
- data/lib/rigor/configuration.rb +58 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +67 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +119 -9
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +64 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +174 -6
- data/lib/rigor/inference/narrowing.rb +103 -1
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +209 -19
- data/lib/rigor/inference/statement_evaluator.rb +172 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +10 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +4 -1
- metadata +56 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../inference/hkt_registry"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Plugin
|
|
5
7
|
# Value object describing one plugin's identity and metadata.
|
|
@@ -11,7 +13,7 @@ module Rigor
|
|
|
11
13
|
# The fields are pinned by ADR-2 § "Registration, Configuration,
|
|
12
14
|
# and Caching"; the v0.1.0 plugin contract surface treats this
|
|
13
15
|
# struct as the public manifest shape.
|
|
14
|
-
class Manifest
|
|
16
|
+
class Manifest # rubocop:disable Metrics/ClassLength
|
|
15
17
|
# Same regex {Rigor::Cache::Store::VALID_PRODUCER_ID} uses,
|
|
16
18
|
# so plugin ids round-trip through cache producer ids and
|
|
17
19
|
# `plugin.<id>.<rule>` diagnostic identifiers without escape.
|
|
@@ -39,13 +41,14 @@ module Rigor
|
|
|
39
41
|
|
|
40
42
|
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
|
|
41
43
|
:owns_receivers, :type_node_resolvers, :block_as_methods, :heredoc_templates,
|
|
42
|
-
:trait_registries, :external_files
|
|
44
|
+
:trait_registries, :external_files, :hkt_registrations, :hkt_definitions
|
|
43
45
|
|
|
44
46
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
45
47
|
id:, version:,
|
|
46
48
|
description: nil, protocols: [], config_schema: {},
|
|
47
49
|
produces: [], consumes: [], owns_receivers: [], type_node_resolvers: [],
|
|
48
|
-
block_as_methods: [], heredoc_templates: [], trait_registries: [], external_files: []
|
|
50
|
+
block_as_methods: [], heredoc_templates: [], trait_registries: [], external_files: [],
|
|
51
|
+
hkt_registrations: [], hkt_definitions: []
|
|
49
52
|
)
|
|
50
53
|
validate_id!(id)
|
|
51
54
|
validate_version!(version)
|
|
@@ -58,9 +61,12 @@ module Rigor
|
|
|
58
61
|
validate_heredoc_templates!(heredoc_templates)
|
|
59
62
|
validate_trait_registries!(trait_registries)
|
|
60
63
|
validate_external_files!(external_files)
|
|
64
|
+
validate_hkt_registrations!(hkt_registrations)
|
|
65
|
+
validate_hkt_definitions!(hkt_definitions)
|
|
61
66
|
|
|
62
67
|
assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
63
|
-
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files
|
|
68
|
+
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files,
|
|
69
|
+
hkt_registrations, hkt_definitions)
|
|
64
70
|
freeze
|
|
65
71
|
end
|
|
66
72
|
|
|
@@ -68,7 +74,8 @@ module Rigor
|
|
|
68
74
|
|
|
69
75
|
# rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
70
76
|
def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
71
|
-
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files
|
|
77
|
+
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files,
|
|
78
|
+
hkt_registrations, hkt_definitions)
|
|
72
79
|
@id = id.dup.freeze
|
|
73
80
|
@version = version.dup.freeze
|
|
74
81
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
@@ -82,6 +89,8 @@ module Rigor
|
|
|
82
89
|
@heredoc_templates = heredoc_templates.dup.freeze
|
|
83
90
|
@trait_registries = trait_registries.dup.freeze
|
|
84
91
|
@external_files = external_files.dup.freeze
|
|
92
|
+
@hkt_registrations = hkt_registrations.dup.freeze
|
|
93
|
+
@hkt_definitions = hkt_definitions.dup.freeze
|
|
85
94
|
end
|
|
86
95
|
# rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
|
|
87
96
|
|
|
@@ -110,7 +119,7 @@ module Rigor
|
|
|
110
119
|
errors
|
|
111
120
|
end
|
|
112
121
|
|
|
113
|
-
def to_h
|
|
122
|
+
def to_h # rubocop:disable Metrics/AbcSize
|
|
114
123
|
{
|
|
115
124
|
"id" => id,
|
|
116
125
|
"version" => version,
|
|
@@ -124,7 +133,9 @@ module Rigor
|
|
|
124
133
|
"block_as_methods" => block_as_methods.map(&:to_h),
|
|
125
134
|
"heredoc_templates" => heredoc_templates.map(&:to_h),
|
|
126
135
|
"trait_registries" => trait_registries.map(&:to_h),
|
|
127
|
-
"external_files" => external_files.map(&:to_h)
|
|
136
|
+
"external_files" => external_files.map(&:to_h),
|
|
137
|
+
"hkt_registrations" => hkt_registrations.map(&:to_h),
|
|
138
|
+
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } }
|
|
128
139
|
}
|
|
129
140
|
end
|
|
130
141
|
|
|
@@ -279,6 +290,42 @@ module Rigor
|
|
|
279
290
|
"Rigor::Plugin::Macro::ExternalFile instances, got #{entries.inspect}"
|
|
280
291
|
end
|
|
281
292
|
|
|
293
|
+
# ADR-20 slice 6 — `hkt_registrations:` declares the
|
|
294
|
+
# Lightweight HKT URI registrations this plugin ships
|
|
295
|
+
# (analogous to `%a{rigor:v1:hkt_register: ...}` directives
|
|
296
|
+
# but published via the manifest contract instead of a
|
|
297
|
+
# shipped `.rbs` file). Each entry MUST be an
|
|
298
|
+
# `Rigor::Inference::HktRegistry::Registration`. The
|
|
299
|
+
# registry aggregator on `Plugin::Registry` flattens
|
|
300
|
+
# entries from every loaded plugin and merges them into
|
|
301
|
+
# `env.hkt_registry` on top of `Builtins::HktBuiltins.registry`;
|
|
302
|
+
# user `.rbs` overlays merge on top of plugin entries
|
|
303
|
+
# last-write-wins.
|
|
304
|
+
def validate_hkt_registrations!(entries)
|
|
305
|
+
return if entries.is_a?(Array) && entries.all?(Inference::HktRegistry::Registration)
|
|
306
|
+
|
|
307
|
+
raise ArgumentError,
|
|
308
|
+
"plugin manifest hkt_registrations must be an Array of " \
|
|
309
|
+
"Rigor::Inference::HktRegistry::Registration instances, got #{entries.inspect}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# ADR-20 slice 6 — `hkt_definitions:` declares the
|
|
313
|
+
# plugin's HKT type-function bodies (analogous to
|
|
314
|
+
# `%a{rigor:v1:hkt_define: ...}` directives). Each entry
|
|
315
|
+
# MUST be an `Rigor::Inference::HktRegistry::Definition`
|
|
316
|
+
# — typically built via
|
|
317
|
+
# `Rigor::Inference::HktRegistry.definition_with_body_tree(...)`
|
|
318
|
+
# so plugin authors can build the body programmatically
|
|
319
|
+
# via {Rigor::Inference::HktBody}'s node-constructor API
|
|
320
|
+
# without parsing a string.
|
|
321
|
+
def validate_hkt_definitions!(entries)
|
|
322
|
+
return if entries.is_a?(Array) && entries.all?(Inference::HktRegistry::Definition)
|
|
323
|
+
|
|
324
|
+
raise ArgumentError,
|
|
325
|
+
"plugin manifest hkt_definitions must be an Array of " \
|
|
326
|
+
"Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
|
|
327
|
+
end
|
|
328
|
+
|
|
282
329
|
def coerce_consumes(consumes)
|
|
283
330
|
unless consumes.is_a?(Array)
|
|
284
331
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
@@ -85,6 +85,25 @@ module Rigor
|
|
|
85
85
|
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# ADR-20 slice 6 — aggregate every loaded plugin's
|
|
89
|
+
# manifest-declared HKT registrations + definitions
|
|
90
|
+
# into a single `Inference::HktRegistry` overlay that
|
|
91
|
+
# `Environment#hkt_registry` merges on top of the
|
|
92
|
+
# bundled `Builtins::HktBuiltins.registry`. Last
|
|
93
|
+
# plugin to register a URI wins (registration order
|
|
94
|
+
# determined by the user's `plugins:` list); user
|
|
95
|
+
# `.rbs` overlays merge on top of this overlay last.
|
|
96
|
+
# Returns `Inference::HktRegistry::EMPTY` when no
|
|
97
|
+
# plugin contributes HKT entries so callers can skip
|
|
98
|
+
# the merge.
|
|
99
|
+
def hkt_overlay_registry
|
|
100
|
+
registrations = plugins.flat_map { |plugin| plugin.manifest.hkt_registrations }
|
|
101
|
+
definitions = plugins.flat_map { |plugin| plugin.manifest.hkt_definitions }
|
|
102
|
+
return Inference::HktRegistry::EMPTY if registrations.empty? && definitions.empty?
|
|
103
|
+
|
|
104
|
+
Inference::HktRegistry.new(registrations: registrations, definitions: definitions)
|
|
105
|
+
end
|
|
106
|
+
|
|
88
107
|
EMPTY = new.freeze
|
|
89
108
|
end
|
|
90
109
|
end
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../inference/hkt_registry"
|
|
4
|
+
require_relative "../inference/hkt_body_parser"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module RbsExtended
|
|
8
|
+
# ADR-20 § "Decision D6" parser for the two new HKT
|
|
9
|
+
# directives that live in `.rbs` files at module / class
|
|
10
|
+
# scope:
|
|
11
|
+
#
|
|
12
|
+
# - `%a{rigor:v1:hkt_register: uri=<uri> arity=<int>
|
|
13
|
+
# variance=<v1>,<v2>,... bound=<class_name_or_untyped>}` —
|
|
14
|
+
# registers a defunctionalised type-constructor URI
|
|
15
|
+
# together with its arity, per-position variance, and
|
|
16
|
+
# erasure bound.
|
|
17
|
+
# - `%a{rigor:v1:hkt_define: uri=<uri> params=<P1>,<P2>,...
|
|
18
|
+
# body=<body_text>}` — binds the URI to a type-function
|
|
19
|
+
# body that {HktBodyParser} parses into an
|
|
20
|
+
# {HktBody::Union} tree.
|
|
21
|
+
#
|
|
22
|
+
# ## Payload format
|
|
23
|
+
#
|
|
24
|
+
# **Space-separated `key=value` pairs.** The format is
|
|
25
|
+
# constrained by RBS's `%a{...}` annotation grammar, which
|
|
26
|
+
# does NOT accept arbitrary nested punctuation (a JSON
|
|
27
|
+
# payload with quotes / nested braces will fail RBS
|
|
28
|
+
# parsing). Each value is a bare token: no quoting, no
|
|
29
|
+
# escaping. Values that contain spaces or `=` signs MUST
|
|
30
|
+
# be encoded via the `body=` key, which is special-cased
|
|
31
|
+
# to gobble everything from `body=` to the end of the
|
|
32
|
+
# payload — see `parse_define`.
|
|
33
|
+
#
|
|
34
|
+
# Example annotations (write inside a class / module
|
|
35
|
+
# declaration so the annotation attaches to the decl
|
|
36
|
+
# RBS parses):
|
|
37
|
+
#
|
|
38
|
+
# %a{rigor:v1:hkt_register: uri=json::value arity=1
|
|
39
|
+
# variance=out bound=untyped}
|
|
40
|
+
# %a{rigor:v1:hkt_define: uri=json::value params=K
|
|
41
|
+
# body=nil | true | false | Integer | Float | String |
|
|
42
|
+
# Array[App[json::value, K]] |
|
|
43
|
+
# Hash[K, App[json::value, K]]}
|
|
44
|
+
# module JsonOverlay
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# ## Bound vocabulary
|
|
48
|
+
#
|
|
49
|
+
# - `untyped` resolves to `Rigor::Type::Combinator.untyped`
|
|
50
|
+
# (i.e. `Dynamic[Top]`, the ADR-20 WD2 default).
|
|
51
|
+
# - A bare class name (`String`, `Integer`, …) resolves
|
|
52
|
+
# through `name_scope.nominal_for_name(...)` when
|
|
53
|
+
# supplied, falling back to a raw `Rigor::Type::Nominal`
|
|
54
|
+
# otherwise.
|
|
55
|
+
# - Anything else falls back to `untyped` and emits an
|
|
56
|
+
# `:info` diagnostic via the supplied reporter (fail-soft
|
|
57
|
+
# so an unrecognised bound never crashes the loader).
|
|
58
|
+
#
|
|
59
|
+
# Richer bound forms (parameterised generics, unions,
|
|
60
|
+
# refinements) wait for a follow-up slice's expression
|
|
61
|
+
# parser.
|
|
62
|
+
module HktDirectives
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
REGISTER_DIRECTIVE = "rigor:v1:hkt_register:"
|
|
66
|
+
DEFINE_DIRECTIVE = "rigor:v1:hkt_define:"
|
|
67
|
+
|
|
68
|
+
DEFAULT_VARIANCE = :inv
|
|
69
|
+
DEFAULT_BOUND_LITERAL = "untyped"
|
|
70
|
+
|
|
71
|
+
# Parses one `%a{rigor:v1:hkt_register: ...}` payload
|
|
72
|
+
# string and returns a `Registration`, or `nil` when the
|
|
73
|
+
# string is not an hkt_register directive (so callers can
|
|
74
|
+
# walk a list of annotations without each having to
|
|
75
|
+
# pre-filter).
|
|
76
|
+
def parse_register(string, name_scope: nil, reporter: nil, source_location: nil)
|
|
77
|
+
payload = extract_payload(string, REGISTER_DIRECTIVE)
|
|
78
|
+
return nil if payload.nil?
|
|
79
|
+
|
|
80
|
+
kvs = parse_kv_payload(payload, body_key: nil)
|
|
81
|
+
|
|
82
|
+
uri = symbolize_uri(kvs["uri"], reporter: reporter, source_location: source_location)
|
|
83
|
+
return nil if uri.nil?
|
|
84
|
+
|
|
85
|
+
arity = coerce_arity(kvs["arity"], reporter: reporter, source_location: source_location)
|
|
86
|
+
return nil if arity.nil?
|
|
87
|
+
|
|
88
|
+
variance = coerce_variance(kvs["variance"], arity, reporter: reporter, source_location: source_location)
|
|
89
|
+
return nil if variance.nil?
|
|
90
|
+
|
|
91
|
+
bound = resolve_bound(
|
|
92
|
+
kvs["bound"] || DEFAULT_BOUND_LITERAL,
|
|
93
|
+
name_scope: name_scope,
|
|
94
|
+
reporter: reporter,
|
|
95
|
+
source_location: source_location
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
Inference::HktRegistry::Registration.new(
|
|
99
|
+
uri: uri,
|
|
100
|
+
arity: arity,
|
|
101
|
+
variance: variance,
|
|
102
|
+
bound: bound
|
|
103
|
+
)
|
|
104
|
+
rescue ArgumentError => e
|
|
105
|
+
record_hkt_error(reporter, "hkt_register: #{e.message}", source_location)
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Parses one `%a{rigor:v1:hkt_define: ...}` payload
|
|
110
|
+
# string and returns a `Definition`, or `nil` when the
|
|
111
|
+
# string is not an hkt_define directive.
|
|
112
|
+
def parse_define(string, reporter: nil, source_location: nil)
|
|
113
|
+
payload = extract_payload(string, DEFINE_DIRECTIVE)
|
|
114
|
+
return nil if payload.nil?
|
|
115
|
+
|
|
116
|
+
kvs = parse_kv_payload(payload, body_key: "body")
|
|
117
|
+
|
|
118
|
+
uri = symbolize_uri(kvs["uri"], reporter: reporter, source_location: source_location)
|
|
119
|
+
return nil if uri.nil?
|
|
120
|
+
|
|
121
|
+
params = coerce_params(kvs["params"], reporter: reporter, source_location: source_location)
|
|
122
|
+
return nil if params.nil?
|
|
123
|
+
|
|
124
|
+
body = kvs["body"]
|
|
125
|
+
unless body.is_a?(String)
|
|
126
|
+
record_hkt_error(reporter, "hkt_define: missing body=", source_location)
|
|
127
|
+
return nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
body_tree = parse_body_tree(body, params, reporter: reporter, source_location: source_location)
|
|
131
|
+
|
|
132
|
+
Inference::HktRegistry::Definition.new(
|
|
133
|
+
uri: uri,
|
|
134
|
+
params: params,
|
|
135
|
+
body: body,
|
|
136
|
+
body_tree: body_tree,
|
|
137
|
+
source_path: source_path_of(source_location),
|
|
138
|
+
source_line: source_line_of(source_location)
|
|
139
|
+
)
|
|
140
|
+
rescue ArgumentError => e
|
|
141
|
+
record_hkt_error(reporter, "hkt_define: #{e.message}", source_location)
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def extract_payload(string, directive)
|
|
146
|
+
return nil if string.nil?
|
|
147
|
+
|
|
148
|
+
idx = string.index(directive)
|
|
149
|
+
return nil if idx.nil?
|
|
150
|
+
|
|
151
|
+
payload = string[(idx + directive.size)..].to_s.strip
|
|
152
|
+
# Strip trailing `}` of the wrapping `%a{...}` form if
|
|
153
|
+
# the caller passed the raw annotation string. The
|
|
154
|
+
# parser also accepts a pre-extracted payload.
|
|
155
|
+
payload = payload.sub(/\}\z/, "") if payload.end_with?("}") && !balanced_braces?(payload)
|
|
156
|
+
payload.empty? ? nil : payload
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def balanced_braces?(string)
|
|
160
|
+
depth = 0
|
|
161
|
+
string.each_char do |ch|
|
|
162
|
+
case ch
|
|
163
|
+
when "{" then depth += 1
|
|
164
|
+
when "}"
|
|
165
|
+
depth -= 1
|
|
166
|
+
return false if depth.negative?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
depth.zero?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Parses a space-separated `key=value [key=value ...]`
|
|
173
|
+
# payload into a Hash. When `body_key` is supplied AND
|
|
174
|
+
# that key appears, everything from `<body_key>=` to
|
|
175
|
+
# the end of the payload becomes the value (body
|
|
176
|
+
# contents typically include spaces, `|`, `[]` etc.
|
|
177
|
+
# that the simple tokenizer cannot otherwise carry).
|
|
178
|
+
KV_KEY_PATTERN = /(?<![\w.])([a-z_]\w*)=/
|
|
179
|
+
private_constant :KV_KEY_PATTERN
|
|
180
|
+
|
|
181
|
+
def parse_kv_payload(payload, body_key:)
|
|
182
|
+
result = {}
|
|
183
|
+
# Find every `<key>=` boundary; each value runs to
|
|
184
|
+
# the next boundary or end of string.
|
|
185
|
+
markers = []
|
|
186
|
+
payload.scan(KV_KEY_PATTERN) { markers << [::Regexp.last_match[1], ::Regexp.last_match.end(0)] }
|
|
187
|
+
markers.each_with_index do |(key, value_start), i|
|
|
188
|
+
if body_key && key == body_key
|
|
189
|
+
result[key] = payload[value_start..].to_s.strip
|
|
190
|
+
break
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
value_end = markers[i + 1] ? markers[i + 1][1] - markers[i + 1][0].size - 1 : payload.size
|
|
194
|
+
result[key] = payload[value_start...value_end].to_s.strip
|
|
195
|
+
end
|
|
196
|
+
result
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ADR-20 slice 2b — parse the body String into an
|
|
200
|
+
# `HktBody::*` tree via {Inference::HktBodyParser.parse}.
|
|
201
|
+
# On parse failure: emit a fail-soft `:info` reporter
|
|
202
|
+
# entry and return `nil` so the resulting Definition
|
|
203
|
+
# keeps its `body` String slot but `body_tree` stays
|
|
204
|
+
# absent (the reducer falls back to `app.bound` at call
|
|
205
|
+
# time per ADR-20 D5).
|
|
206
|
+
def parse_body_tree(body, params, reporter:, source_location:)
|
|
207
|
+
return nil if body.nil? || body.empty?
|
|
208
|
+
|
|
209
|
+
Inference::HktBodyParser.parse(body, params: params)
|
|
210
|
+
rescue Inference::HktBodyParser::ParseError => e
|
|
211
|
+
record_hkt_error(reporter, "hkt_define body parse error: #{e.message}", source_location)
|
|
212
|
+
nil
|
|
213
|
+
rescue ArgumentError => e
|
|
214
|
+
record_hkt_error(reporter, "hkt_define body construction error: #{e.message}", source_location)
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def symbolize_uri(raw, reporter:, source_location:)
|
|
219
|
+
if raw.nil? || raw.empty?
|
|
220
|
+
record_hkt_error(reporter, "uri= is required", source_location)
|
|
221
|
+
return nil
|
|
222
|
+
end
|
|
223
|
+
unless raw.include?(Type::App::URI_SEPARATOR)
|
|
224
|
+
record_hkt_error(
|
|
225
|
+
reporter,
|
|
226
|
+
"uri must be namespaced as `a::b` per ADR-20 WD1, got #{raw.inspect}",
|
|
227
|
+
source_location
|
|
228
|
+
)
|
|
229
|
+
return nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
raw.to_sym
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def coerce_arity(raw, reporter:, source_location:)
|
|
236
|
+
if raw && /\A\d+\z/.match?(raw) && raw.to_i.positive?
|
|
237
|
+
raw.to_i
|
|
238
|
+
else
|
|
239
|
+
record_hkt_error(reporter, "arity must be a positive Integer, got #{raw.inspect}", source_location)
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def coerce_variance(raw, arity, reporter:, source_location:)
|
|
245
|
+
# Omitted variance defaults to `[:inv] * arity` per ADR-20 WD4.
|
|
246
|
+
variance =
|
|
247
|
+
if raw.nil? || raw.empty?
|
|
248
|
+
Array.new(arity, DEFAULT_VARIANCE)
|
|
249
|
+
else
|
|
250
|
+
raw.split(",").map { |v| v.strip.to_sym }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
unless variance.size == arity
|
|
254
|
+
record_hkt_error(reporter, "variance length #{variance.size} does not match arity #{arity}", source_location)
|
|
255
|
+
return nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
unless variance.all? { |v| %i[out in inv].include?(v) }
|
|
259
|
+
record_hkt_error(
|
|
260
|
+
reporter,
|
|
261
|
+
"variance entries must be `out` / `in` / `inv`, got #{variance.inspect}",
|
|
262
|
+
source_location
|
|
263
|
+
)
|
|
264
|
+
return nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
variance
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def coerce_params(raw, reporter:, source_location:)
|
|
271
|
+
if raw.nil? || raw.empty?
|
|
272
|
+
record_hkt_error(reporter, "params= is required (comma-separated UCName list)", source_location)
|
|
273
|
+
return nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
raw.split(",").map { |p| p.strip.to_sym }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def resolve_bound(raw, name_scope:, reporter:, source_location:)
|
|
280
|
+
return Type::Combinator.untyped if raw.nil? || raw.strip.empty?
|
|
281
|
+
return Type::Combinator.untyped if raw.strip == DEFAULT_BOUND_LITERAL
|
|
282
|
+
|
|
283
|
+
class_name = raw.strip
|
|
284
|
+
if /\A(?:::)?(?:[A-Z]\w*)(?:::[A-Z]\w*)*\z/.match?(class_name)
|
|
285
|
+
normalized = class_name.sub(/\A::/, "")
|
|
286
|
+
return Type::Nominal.new(normalized) if name_scope.nil?
|
|
287
|
+
|
|
288
|
+
if name_scope.respond_to?(:nominal_for_name)
|
|
289
|
+
resolved = name_scope.nominal_for_name(normalized)
|
|
290
|
+
return resolved if resolved
|
|
291
|
+
end
|
|
292
|
+
return Type::Nominal.new(normalized)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
record_hkt_error(
|
|
296
|
+
reporter,
|
|
297
|
+
"bound `#{raw}` not recognised (accepts `untyped` or a bare class name); falling back to `untyped`",
|
|
298
|
+
source_location
|
|
299
|
+
)
|
|
300
|
+
Type::Combinator.untyped
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def source_path_of(source_location)
|
|
304
|
+
return nil if source_location.nil?
|
|
305
|
+
|
|
306
|
+
source_location.respond_to?(:name) ? source_location.name : nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def source_line_of(source_location)
|
|
310
|
+
return nil if source_location.nil?
|
|
311
|
+
|
|
312
|
+
source_location.respond_to?(:start_line) ? source_location.start_line : nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def record_hkt_error(reporter, message, source_location)
|
|
316
|
+
return if reporter.nil?
|
|
317
|
+
|
|
318
|
+
if reporter.respond_to?(:record)
|
|
319
|
+
reporter.record(directive: "hkt", message: message, source_location: source_location)
|
|
320
|
+
elsif reporter.respond_to?(:<<)
|
|
321
|
+
reporter << { directive: "hkt", message: message, source_location: source_location }
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "type"
|
|
|
4
4
|
require_relative "builtins/imported_refinements"
|
|
5
5
|
require_relative "flow_contribution"
|
|
6
6
|
require_relative "rbs_extended/reporter"
|
|
7
|
+
require_relative "rbs_extended/hkt_directives"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
# Slice 7 phase 15 — first-preview reader for the
|
|
@@ -385,13 +386,15 @@ module Rigor
|
|
|
385
386
|
|
|
386
387
|
name_scope = environment&.name_scope
|
|
387
388
|
reporter = environment&.rbs_extended_reporter
|
|
389
|
+
hkt_registry = environment&.hkt_registry
|
|
388
390
|
|
|
389
391
|
annotations.each do |annotation|
|
|
390
392
|
type = parse_return_type_override(
|
|
391
393
|
annotation.string,
|
|
392
394
|
name_scope: name_scope,
|
|
393
395
|
reporter: reporter,
|
|
394
|
-
source_location: annotation.location
|
|
396
|
+
source_location: annotation.location,
|
|
397
|
+
hkt_registry: hkt_registry
|
|
395
398
|
)
|
|
396
399
|
return type if type
|
|
397
400
|
end
|
|
@@ -417,10 +420,40 @@ module Rigor
|
|
|
417
420
|
/x
|
|
418
421
|
private_constant :RETURN_DIRECTIVE_PATTERN
|
|
419
422
|
|
|
420
|
-
|
|
423
|
+
# ADR-20 slice 2d — recognises `App[<uri>, <ClassName>, ...]`
|
|
424
|
+
# syntax in a `rigor:v1:return:` payload before falling
|
|
425
|
+
# through to the refinement-name parser. The match captures
|
|
426
|
+
# the namespaced URI (`json::value`) plus a comma-separated
|
|
427
|
+
# list of bare class names (`String`, `Symbol`, `Integer`).
|
|
428
|
+
# Slice 2d keeps the arg vocabulary intentionally narrow;
|
|
429
|
+
# parameterised forms (`Array[T]`, `Hash[K, V]`), unions,
|
|
430
|
+
# and refinements inside `App[...]` wait for a follow-up
|
|
431
|
+
# slice's expression parser.
|
|
432
|
+
APP_PAYLOAD_PATTERN = /
|
|
433
|
+
\A
|
|
434
|
+
App\[
|
|
435
|
+
\s*
|
|
436
|
+
(?<uri>[a-z_][a-z0-9_]*(?:::[a-z_][a-z0-9_]*)+)
|
|
437
|
+
\s*,\s*
|
|
438
|
+
(?<args>[^\[\]]+?)
|
|
439
|
+
\s*\]
|
|
440
|
+
\z
|
|
441
|
+
/x
|
|
442
|
+
private_constant :APP_PAYLOAD_PATTERN
|
|
443
|
+
|
|
444
|
+
def parse_return_type_override(string, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil)
|
|
421
445
|
match = RETURN_DIRECTIVE_PATTERN.match(string)
|
|
422
446
|
return nil if match.nil?
|
|
423
447
|
|
|
448
|
+
app_type = parse_app_payload(
|
|
449
|
+
match[:payload],
|
|
450
|
+
name_scope: name_scope,
|
|
451
|
+
reporter: reporter,
|
|
452
|
+
source_location: source_location,
|
|
453
|
+
hkt_registry: hkt_registry
|
|
454
|
+
)
|
|
455
|
+
return app_type if app_type
|
|
456
|
+
|
|
424
457
|
type = Builtins::ImportedRefinements.parse(
|
|
425
458
|
match[:payload],
|
|
426
459
|
name_scope: name_scope,
|
|
@@ -431,6 +464,53 @@ module Rigor
|
|
|
431
464
|
type
|
|
432
465
|
end
|
|
433
466
|
|
|
467
|
+
# ADR-20 slice 2d. Parses `App[<uri>, <ClassName>, ...]`
|
|
468
|
+
# syntax into a `Rigor::Type::App`. When `hkt_registry` is
|
|
469
|
+
# supplied and the URI is registered with a body_tree, the
|
|
470
|
+
# `App` is reduced eagerly via {Inference::HktRegistry#reduce}
|
|
471
|
+
# so call sites observe the unfolded form (e.g.
|
|
472
|
+
# `Union[nil, true, false, ..., Array[App[json::value,
|
|
473
|
+
# String]], Hash[String, App[json::value, String]]]`)
|
|
474
|
+
# rather than the opaque carrier. When the registry is
|
|
475
|
+
# absent or the URI is unregistered, the carrier with its
|
|
476
|
+
# registry-supplied bound (or `untyped` as a last-resort
|
|
477
|
+
# fallback) is returned as-is.
|
|
478
|
+
def parse_app_payload(payload, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil)
|
|
479
|
+
match = APP_PAYLOAD_PATTERN.match(payload)
|
|
480
|
+
return nil if match.nil?
|
|
481
|
+
|
|
482
|
+
uri = match[:uri].to_sym
|
|
483
|
+
arg_classes = match[:args].split(",").map(&:strip)
|
|
484
|
+
args = arg_classes.map { |name| resolve_app_arg(name, name_scope: name_scope) }
|
|
485
|
+
|
|
486
|
+
if args.any?(&:nil?)
|
|
487
|
+
record_unresolved(reporter, "App payload `#{payload}`: unresolved arg class name", source_location)
|
|
488
|
+
return nil
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
registration = hkt_registry&.registration(uri)
|
|
492
|
+
bound = registration&.bound || Type::Combinator.untyped
|
|
493
|
+
app = Type::App.new(uri, args, bound: bound)
|
|
494
|
+
|
|
495
|
+
return app if hkt_registry.nil? || !hkt_registry.defined?(uri)
|
|
496
|
+
|
|
497
|
+
reduced = hkt_registry.reduce(app)
|
|
498
|
+
reduced || app
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def resolve_app_arg(class_name, name_scope: nil)
|
|
502
|
+
return nil unless /\A(?:::)?(?:[A-Z]\w*)(?:::[A-Z]\w*)*\z/.match?(class_name)
|
|
503
|
+
|
|
504
|
+
normalized = class_name.sub(/\A::/, "")
|
|
505
|
+
return Type::Nominal.new(normalized) if name_scope.nil?
|
|
506
|
+
|
|
507
|
+
if name_scope.respond_to?(:nominal_for_name)
|
|
508
|
+
resolved = name_scope.nominal_for_name(normalized)
|
|
509
|
+
return resolved if resolved
|
|
510
|
+
end
|
|
511
|
+
Type::Nominal.new(normalized)
|
|
512
|
+
end
|
|
513
|
+
|
|
434
514
|
# Returned for `rigor:v1:param: <name> <refinement>`. The
|
|
435
515
|
# parameter name is a Ruby identifier (Symbol); the type
|
|
436
516
|
# is any `Rigor::Type` the refinement parser resolves
|
|
@@ -52,6 +52,15 @@ module Rigor
|
|
|
52
52
|
@paths = paths
|
|
53
53
|
@observations = normalize_observations(observations)
|
|
54
54
|
@include_private = include_private
|
|
55
|
+
# Per-file scratch state. `analyse_file` resets each
|
|
56
|
+
# one to a fresh container for every file walked so
|
|
57
|
+
# candidates from one file don't leak into another;
|
|
58
|
+
# initialising empty here gives downstream consumers
|
|
59
|
+
# (`build_candidate`, `method_def_prefix`) a never-nil
|
|
60
|
+
# invariant without per-call-site defensive guards.
|
|
61
|
+
@namespace_kinds = {}
|
|
62
|
+
@module_function_methods = Set.new
|
|
63
|
+
@class_shells = Set.new
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
# Lifts legacy plain-`Array[Type]` observation entries
|
|
@@ -270,8 +279,8 @@ module Rigor
|
|
|
270
279
|
# `Const = Data.define(...)` declarations.
|
|
271
280
|
def build_candidate(**)
|
|
272
281
|
MethodCandidate.new(
|
|
273
|
-
namespace_kinds: @namespace_kinds
|
|
274
|
-
class_shells:
|
|
282
|
+
namespace_kinds: @namespace_kinds,
|
|
283
|
+
class_shells: @class_shells.to_a,
|
|
275
284
|
**
|
|
276
285
|
)
|
|
277
286
|
end
|
|
@@ -282,7 +291,7 @@ module Rigor
|
|
|
282
291
|
# dispatch at runtime), or "def " (plain instance).
|
|
283
292
|
def method_def_prefix(class_name, method_name, kind)
|
|
284
293
|
return "def self." if kind == :singleton
|
|
285
|
-
return "def self?." if @module_function_methods
|
|
294
|
+
return "def self?." if @module_function_methods.include?([class_name, method_name])
|
|
286
295
|
|
|
287
296
|
"def "
|
|
288
297
|
end
|