rigortype 0.1.8 → 0.1.10
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/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +274 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli.rb +134 -6
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +168 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +308 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +178 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- 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 +431 -9
- 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/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +29 -3
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +67 -5
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +101 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +87 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/triage/catalogue.rb +2 -2
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +42 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module MCP
|
|
7
|
+
# Reads newline-delimited JSON-RPC 2.0 messages from `input`,
|
|
8
|
+
# dispatches each to `server`, and writes responses to `output`.
|
|
9
|
+
# Runs until input reaches EOF (the client closes the connection).
|
|
10
|
+
class Loop
|
|
11
|
+
def initialize(input:, output:, server:)
|
|
12
|
+
@input = input
|
|
13
|
+
@output = output
|
|
14
|
+
@server = server
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
@input.each_line do |raw|
|
|
19
|
+
line = raw.chomp
|
|
20
|
+
next if line.empty?
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
request = JSON.parse(line)
|
|
24
|
+
rescue JSON::ParserError => e
|
|
25
|
+
write_response({ "jsonrpc" => "2.0", "id" => nil,
|
|
26
|
+
"error" => { "code" => -32_700, "message" => "Parse error: #{e.message}" } })
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
response = @server.handle(request)
|
|
31
|
+
write_response(response) if response
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def write_response(response)
|
|
38
|
+
@output.puts(JSON.generate(response))
|
|
39
|
+
@output.flush
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module MCP
|
|
8
|
+
# JSON-RPC 2.0 dispatcher for the MCP server.
|
|
9
|
+
#
|
|
10
|
+
# Each public `handle` call takes a parsed request hash and returns
|
|
11
|
+
# a response hash (or nil for notifications that require no reply).
|
|
12
|
+
# Tool implementations delegate to `CLI.new(argv, out:, err:).run`
|
|
13
|
+
# with StringIO capture — every tool stays in sync with its CLI
|
|
14
|
+
# counterpart automatically (ADR-33 WD4).
|
|
15
|
+
class Server # rubocop:disable Metrics/ClassLength
|
|
16
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
17
|
+
|
|
18
|
+
TOOLS = [
|
|
19
|
+
{
|
|
20
|
+
name: "rigor_check",
|
|
21
|
+
description: "Analyze Ruby files for type errors, undefined methods, arity mismatches, " \
|
|
22
|
+
"and nil-receiver risks. Returns a JSON diagnostic report.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
paths: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "string" },
|
|
29
|
+
description: "Files or directories to analyze (required)"
|
|
30
|
+
},
|
|
31
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
32
|
+
},
|
|
33
|
+
required: ["paths"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "rigor_type_of",
|
|
38
|
+
description: "Get the inferred type of the expression at a specific location in a Ruby file.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
file: { type: "string", description: "Path to the Ruby file" },
|
|
43
|
+
line: { type: "integer", description: "1-based line number" },
|
|
44
|
+
col: { type: "integer", description: "1-based column number" },
|
|
45
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
46
|
+
},
|
|
47
|
+
required: %w[file line col]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "rigor_triage",
|
|
52
|
+
description: "Summarize a project's diagnostics: rule distribution, per-file hotspots, " \
|
|
53
|
+
"and heuristic hints for the most common error clusters. Returns JSON. " \
|
|
54
|
+
"Useful for understanding the shape of a diagnostic set before deciding what to fix.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
paths: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: { type: "string" },
|
|
61
|
+
description: "Files or directories to analyze (defaults to configured paths)"
|
|
62
|
+
},
|
|
63
|
+
top: { type: "integer", description: "Number of hotspot files to include (default: 10)" },
|
|
64
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "rigor_annotate",
|
|
70
|
+
description: "Return the given Ruby source file with each line's last-expression type " \
|
|
71
|
+
"appended as a comment. Useful for understanding how Rigor infers types " \
|
|
72
|
+
"through a file.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
file: { type: "string", description: "Path to the Ruby file to annotate" },
|
|
77
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
78
|
+
},
|
|
79
|
+
required: ["file"]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "rigor_sig_gen",
|
|
84
|
+
description: "Generate RBS skeleton signatures inferred from Ruby source files. " \
|
|
85
|
+
"Returns a JSON report of candidates with their classifications " \
|
|
86
|
+
"(new-file, new-method, tighter-return, equivalent, skipped).",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
paths: {
|
|
91
|
+
type: "array",
|
|
92
|
+
items: { type: "string" },
|
|
93
|
+
description: "Files or directories to generate signatures for (defaults to configured paths)"
|
|
94
|
+
},
|
|
95
|
+
params: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: %w[untyped observed],
|
|
98
|
+
description: "Parameter policy: untyped (default) or observed " \
|
|
99
|
+
"(harvests call-site argument types from spec/)"
|
|
100
|
+
},
|
|
101
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "rigor_explain",
|
|
107
|
+
description: "Look up the description of one or all Rigor diagnostic rules. " \
|
|
108
|
+
"Returns JSON. Without a rule argument, returns the full catalog.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
rule: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Rule ID, legacy alias, or family prefix (call, flow, assert, dump, def). " \
|
|
115
|
+
"Omit to list every rule."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "rigor_coverage",
|
|
122
|
+
description: "Report type-precision coverage: the ratio of expressions Rigor types as " \
|
|
123
|
+
"Constant / Nominal / shaped / refined (precise) vs Dynamic or top (opaque). " \
|
|
124
|
+
"Returns JSON. Useful for measuring the impact of adding new fold rules.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
paths: {
|
|
129
|
+
type: "array",
|
|
130
|
+
items: { type: "string" },
|
|
131
|
+
description: "Files or directories to scan (required)"
|
|
132
|
+
},
|
|
133
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
134
|
+
},
|
|
135
|
+
required: ["paths"]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
].freeze
|
|
139
|
+
|
|
140
|
+
def initialize(config_path: nil, err: $stderr)
|
|
141
|
+
@config_path = config_path
|
|
142
|
+
@err = err
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Dispatches a parsed JSON-RPC request hash.
|
|
146
|
+
# Returns nil for notifications (requests without an `id`).
|
|
147
|
+
def handle(request)
|
|
148
|
+
id = request["id"]
|
|
149
|
+
method_name = request["method"]
|
|
150
|
+
|
|
151
|
+
# Notifications carry no `id` and require no response.
|
|
152
|
+
return nil if id.nil?
|
|
153
|
+
|
|
154
|
+
case method_name
|
|
155
|
+
when "initialize" then handle_initialize(id)
|
|
156
|
+
when "ping" then success(id, {})
|
|
157
|
+
when "tools/list" then success(id, { tools: TOOLS })
|
|
158
|
+
when "tools/call"
|
|
159
|
+
call_tool(id,
|
|
160
|
+
request.dig("params", "name"),
|
|
161
|
+
request.dig("params", "arguments") || {})
|
|
162
|
+
else
|
|
163
|
+
error(id, -32_601, "Method not found: #{method_name.inspect}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def handle_initialize(id)
|
|
170
|
+
require_relative "../version"
|
|
171
|
+
success(id, {
|
|
172
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
173
|
+
capabilities: { tools: { listChanged: false } },
|
|
174
|
+
serverInfo: { name: "rigor", version: Rigor::VERSION }
|
|
175
|
+
})
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def call_tool(id, name, args)
|
|
179
|
+
argv = build_argv(name, args)
|
|
180
|
+
return error(id, -32_602, "Unknown tool: #{name.inspect}") unless argv
|
|
181
|
+
|
|
182
|
+
out_io = StringIO.new
|
|
183
|
+
err_io = StringIO.new
|
|
184
|
+
require_relative "../cli"
|
|
185
|
+
exit_code = CLI.new(argv, out: out_io, err: err_io).run
|
|
186
|
+
|
|
187
|
+
is_error = exit_code == CLI::EXIT_USAGE
|
|
188
|
+
text = out_io.string
|
|
189
|
+
text = err_io.string if text.empty? && is_error
|
|
190
|
+
|
|
191
|
+
success(id, { content: [{ type: "text", text: text }], isError: is_error })
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
@err.puts("rigor mcp: #{e.class}: #{e.message}")
|
|
194
|
+
@err.puts(e.backtrace.first(5).join("\n")) if e.backtrace
|
|
195
|
+
success(id, {
|
|
196
|
+
content: [{ type: "text", text: "Internal error: #{e.message}" }],
|
|
197
|
+
isError: true
|
|
198
|
+
})
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def build_argv(name, args) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
202
|
+
effective_config = args["config"] || @config_path
|
|
203
|
+
|
|
204
|
+
case name
|
|
205
|
+
when "rigor_check"
|
|
206
|
+
argv = ["check", "--format=json", "--no-stats"]
|
|
207
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
208
|
+
argv += Array(args["paths"])
|
|
209
|
+
argv
|
|
210
|
+
|
|
211
|
+
when "rigor_type_of"
|
|
212
|
+
return nil unless args["file"] && args["line"] && args["col"]
|
|
213
|
+
|
|
214
|
+
argv = ["type-of", "--format=json"]
|
|
215
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
216
|
+
argv << "#{args['file']}:#{args['line']}:#{args['col']}"
|
|
217
|
+
argv
|
|
218
|
+
|
|
219
|
+
when "rigor_triage"
|
|
220
|
+
argv = ["triage", "--format=json"]
|
|
221
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
222
|
+
argv << "--top=#{args['top']}" if args["top"]
|
|
223
|
+
argv += Array(args["paths"])
|
|
224
|
+
argv
|
|
225
|
+
|
|
226
|
+
when "rigor_annotate"
|
|
227
|
+
return nil unless args["file"]
|
|
228
|
+
|
|
229
|
+
argv = ["annotate", "--no-color"]
|
|
230
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
231
|
+
argv << args["file"]
|
|
232
|
+
argv
|
|
233
|
+
|
|
234
|
+
when "rigor_sig_gen"
|
|
235
|
+
argv = ["sig-gen", "--print", "--format=json"]
|
|
236
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
237
|
+
argv << "--params=#{args['params']}" if args["params"]
|
|
238
|
+
argv += Array(args["paths"])
|
|
239
|
+
argv
|
|
240
|
+
|
|
241
|
+
when "rigor_explain"
|
|
242
|
+
argv = ["explain", "--format=json"]
|
|
243
|
+
argv << args["rule"] if args["rule"]
|
|
244
|
+
argv
|
|
245
|
+
|
|
246
|
+
when "rigor_coverage"
|
|
247
|
+
argv = ["coverage", "--format=json"]
|
|
248
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
249
|
+
argv += Array(args["paths"])
|
|
250
|
+
argv
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def success(id, result)
|
|
255
|
+
{ "jsonrpc" => "2.0", "id" => id, "result" => result }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def error(id, code, message)
|
|
259
|
+
{ "jsonrpc" => "2.0", "id" => id, "error" => { "code" => code, "message" => message } }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
data/lib/rigor/mcp.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# The MCP (Model Context Protocol) server subsystem.
|
|
5
|
+
# See `docs/adr/33-mcp-server.md` for the design.
|
|
6
|
+
#
|
|
7
|
+
# Entry point: `rigor mcp --transport stdio`.
|
|
8
|
+
# The server exposes Rigor's analysis tools (check, type-of, triage,
|
|
9
|
+
# annotate, sig-gen, explain, coverage) as MCP tool calls over a
|
|
10
|
+
# newline-delimited JSON-RPC 2.0 stdio stream.
|
|
11
|
+
module MCP
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "mcp/server"
|
|
16
|
+
require_relative "mcp/loop"
|
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
|
|
@@ -312,11 +351,6 @@ module Rigor
|
|
|
312
351
|
# descriptor from (1) the plugin's PluginEntry template
|
|
313
352
|
# and (2) the IoBoundary's accumulated FileEntry rows.
|
|
314
353
|
def build_plugin_cache_descriptor
|
|
315
|
-
plugin_entry = Cache::Descriptor::PluginEntry.new(
|
|
316
|
-
id: manifest.id,
|
|
317
|
-
version: manifest.version,
|
|
318
|
-
config_hash: digest_config(config)
|
|
319
|
-
)
|
|
320
354
|
boundary_descriptor = io_boundary.cache_descriptor
|
|
321
355
|
Cache::Descriptor.new(
|
|
322
356
|
plugins: [plugin_entry],
|
|
@@ -324,6 +358,34 @@ module Rigor
|
|
|
324
358
|
)
|
|
325
359
|
end
|
|
326
360
|
|
|
361
|
+
public
|
|
362
|
+
|
|
363
|
+
# ADR-32 WD5 — the `Cache::Descriptor::PluginEntry`
|
|
364
|
+
# template carrying this plugin's id, version, and a
|
|
365
|
+
# SHA-256 digest of its (canonicalised) config hash.
|
|
366
|
+
# Callers outside the plugin (e.g. `Environment.for_project`
|
|
367
|
+
# caching per-file synthesizer output) compose this entry
|
|
368
|
+
# into their own cache descriptor so a config change to
|
|
369
|
+
# the plugin (e.g. flipping `require_magic_comment:`)
|
|
370
|
+
# invalidates the dependent cache.
|
|
371
|
+
def plugin_entry
|
|
372
|
+
# Built fresh on each call rather than memoised so a
|
|
373
|
+
# plugin subclass that freezes itself in `initialize`
|
|
374
|
+
# (e.g. `Rigor::Plugin::RbsInline` per ADR-32) doesn't
|
|
375
|
+
# trip a FrozenError on first read. The construction
|
|
376
|
+
# cost is a single `Data.define`-backed value-object
|
|
377
|
+
# build; the cache key derivation downstream is the
|
|
378
|
+
# expensive step, and it's already memoised inside
|
|
379
|
+
# `Cache::Store`.
|
|
380
|
+
Cache::Descriptor::PluginEntry.new(
|
|
381
|
+
id: manifest.id,
|
|
382
|
+
version: manifest.version,
|
|
383
|
+
config_hash: digest_config(config)
|
|
384
|
+
)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private
|
|
388
|
+
|
|
327
389
|
# ADR-7 § "Slice 6" follow-up — composes the auto-built
|
|
328
390
|
# cache descriptor with an optional plugin-author-supplied
|
|
329
391
|
# extension. Extra `GemEntry` / `FileEntry` / `ConfigEntry`
|
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,17 @@ 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, :source_rbs_synthesizer
|
|
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: [],
|
|
54
|
+
source_rbs_synthesizer: nil
|
|
52
55
|
)
|
|
53
56
|
validate_id!(id)
|
|
54
57
|
validate_version!(version)
|
|
@@ -56,6 +59,7 @@ module Rigor
|
|
|
56
59
|
validate_config_schema!(config_schema)
|
|
57
60
|
validate_produces!(produces)
|
|
58
61
|
validate_owns_receivers!(owns_receivers)
|
|
62
|
+
validate_open_receivers!(open_receivers)
|
|
59
63
|
validate_type_node_resolvers!(type_node_resolvers)
|
|
60
64
|
validate_block_as_methods!(block_as_methods)
|
|
61
65
|
validate_heredoc_templates!(heredoc_templates)
|
|
@@ -63,10 +67,14 @@ module Rigor
|
|
|
63
67
|
validate_external_files!(external_files)
|
|
64
68
|
validate_hkt_registrations!(hkt_registrations)
|
|
65
69
|
validate_hkt_definitions!(hkt_definitions)
|
|
70
|
+
validate_signature_paths!(signature_paths)
|
|
71
|
+
validate_protocol_contracts!(protocol_contracts)
|
|
72
|
+
validate_source_rbs_synthesizer!(source_rbs_synthesizer)
|
|
66
73
|
|
|
67
74
|
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
|
|
75
|
+
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
76
|
+
external_files, hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
|
|
77
|
+
source_rbs_synthesizer)
|
|
70
78
|
freeze
|
|
71
79
|
end
|
|
72
80
|
|
|
@@ -74,8 +82,9 @@ module Rigor
|
|
|
74
82
|
|
|
75
83
|
# rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
76
84
|
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
|
|
85
|
+
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
86
|
+
external_files, hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
|
|
87
|
+
source_rbs_synthesizer)
|
|
79
88
|
@id = id.dup.freeze
|
|
80
89
|
@version = version.dup.freeze
|
|
81
90
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
@@ -84,6 +93,7 @@ module Rigor
|
|
|
84
93
|
@produces = produces.map(&:to_sym).freeze
|
|
85
94
|
@consumes = coerce_consumes(consumes)
|
|
86
95
|
@owns_receivers = owns_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
96
|
+
@open_receivers = open_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
87
97
|
@type_node_resolvers = type_node_resolvers.dup.freeze
|
|
88
98
|
@block_as_methods = block_as_methods.dup.freeze
|
|
89
99
|
@heredoc_templates = heredoc_templates.dup.freeze
|
|
@@ -91,6 +101,9 @@ module Rigor
|
|
|
91
101
|
@external_files = external_files.dup.freeze
|
|
92
102
|
@hkt_registrations = hkt_registrations.dup.freeze
|
|
93
103
|
@hkt_definitions = hkt_definitions.dup.freeze
|
|
104
|
+
@signature_paths = signature_paths.map { |p| p.to_s.dup.freeze }.freeze
|
|
105
|
+
@protocol_contracts = protocol_contracts.dup.freeze
|
|
106
|
+
@source_rbs_synthesizer = source_rbs_synthesizer
|
|
94
107
|
end
|
|
95
108
|
# rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
|
|
96
109
|
|
|
@@ -119,7 +132,7 @@ module Rigor
|
|
|
119
132
|
errors
|
|
120
133
|
end
|
|
121
134
|
|
|
122
|
-
def to_h # rubocop:disable Metrics/AbcSize
|
|
135
|
+
def to_h # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
123
136
|
{
|
|
124
137
|
"id" => id,
|
|
125
138
|
"version" => version,
|
|
@@ -129,13 +142,17 @@ module Rigor
|
|
|
129
142
|
"produces" => produces.map(&:to_s),
|
|
130
143
|
"consumes" => consumes.map { |c| consumption_hash(c) },
|
|
131
144
|
"owns_receivers" => owns_receivers,
|
|
145
|
+
"open_receivers" => open_receivers,
|
|
132
146
|
"type_node_resolvers" => type_node_resolvers.map { |r| r.class.name },
|
|
133
147
|
"block_as_methods" => block_as_methods.map(&:to_h),
|
|
134
148
|
"heredoc_templates" => heredoc_templates.map(&:to_h),
|
|
135
149
|
"trait_registries" => trait_registries.map(&:to_h),
|
|
136
150
|
"external_files" => external_files.map(&:to_h),
|
|
137
151
|
"hkt_registrations" => hkt_registrations.map(&:to_h),
|
|
138
|
-
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } }
|
|
152
|
+
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } },
|
|
153
|
+
"signature_paths" => signature_paths,
|
|
154
|
+
"protocol_contracts" => protocol_contracts.map(&:to_h),
|
|
155
|
+
"source_rbs_synthesizer" => source_rbs_synthesizer&.class&.name
|
|
139
156
|
}
|
|
140
157
|
end
|
|
141
158
|
|
|
@@ -217,6 +234,24 @@ module Rigor
|
|
|
217
234
|
"got #{owns_receivers.inspect}"
|
|
218
235
|
end
|
|
219
236
|
|
|
237
|
+
# ADR-26 — `open_receivers:` declares the class names this
|
|
238
|
+
# plugin marks as "open": statically known to respond beyond
|
|
239
|
+
# their RBS-declared method surface (e.g. `ActiveRecord::Relation`,
|
|
240
|
+
# which delegates an unbounded set of user-defined scopes to
|
|
241
|
+
# its model). `Analysis::CheckRules` skips the
|
|
242
|
+
# `call.undefined-method` rule for a receiver whose class any
|
|
243
|
+
# loaded plugin lists here — flagging a method on a class
|
|
244
|
+
# with an open dynamic surface is unsound. Distinct from
|
|
245
|
+
# `owns_receivers:` (which routes dispatch); this one only
|
|
246
|
+
# suppresses the diagnostic.
|
|
247
|
+
def validate_open_receivers!(open_receivers)
|
|
248
|
+
return if open_receivers.is_a?(Array) && open_receivers.all? { |c| c.is_a?(String) && !c.empty? }
|
|
249
|
+
|
|
250
|
+
raise ArgumentError,
|
|
251
|
+
"plugin manifest open_receivers must be an Array of non-empty String, " \
|
|
252
|
+
"got #{open_receivers.inspect}"
|
|
253
|
+
end
|
|
254
|
+
|
|
220
255
|
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
221
256
|
# plugin-supplied `TypeNodeResolver` instances the parser
|
|
222
257
|
# consults (in slice 3) when an RBS::Extended payload's
|
|
@@ -326,6 +361,62 @@ module Rigor
|
|
|
326
361
|
"Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
|
|
327
362
|
end
|
|
328
363
|
|
|
364
|
+
# ADR-25 — `signature_paths:` declares the RBS signature
|
|
365
|
+
# directories this plugin gem ships, as paths relative to the
|
|
366
|
+
# plugin's own gem root (e.g. `["sig"]`). `Plugin::Base#signature_paths`
|
|
367
|
+
# resolves them to absolute dirs against the gem root; the
|
|
368
|
+
# loader validates each exists and `Environment.for_project`
|
|
369
|
+
# merges the resolved set into the RBS environment.
|
|
370
|
+
def validate_signature_paths!(paths)
|
|
371
|
+
return if paths.is_a?(Array) && paths.all? { |p| p.is_a?(String) && !p.empty? }
|
|
372
|
+
|
|
373
|
+
raise ArgumentError,
|
|
374
|
+
"plugin manifest signature_paths must be an Array of non-empty String, " \
|
|
375
|
+
"got #{paths.inspect}"
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# ADR-28 — `protocol_contracts:` declares the path-scoped
|
|
379
|
+
# method-protocol contracts the plugin contributes. Each
|
|
380
|
+
# entry MUST be a `Rigor::Plugin::ProtocolContract`. The
|
|
381
|
+
# registry aggregator on `Plugin::Registry` flattens
|
|
382
|
+
# contracts across loaded plugins; the engine consults them
|
|
383
|
+
# in two places — `MethodParameterBinder` provides the
|
|
384
|
+
# declared parameter types into matching method bodies, and
|
|
385
|
+
# the contributing plugin's `#diagnostics_for_file` checks
|
|
386
|
+
# method presence + return-type conformance. The manifest
|
|
387
|
+
# field carries the plugin's *default* contracts; a plugin
|
|
388
|
+
# MAY override `Plugin::Base#protocol_contracts` to fold in
|
|
389
|
+
# per-project config (e.g. a custom convention path).
|
|
390
|
+
def validate_protocol_contracts!(entries)
|
|
391
|
+
return if entries.is_a?(Array) && entries.all?(ProtocolContract)
|
|
392
|
+
|
|
393
|
+
raise ArgumentError,
|
|
394
|
+
"plugin manifest protocol_contracts must be an Array of " \
|
|
395
|
+
"Rigor::Plugin::ProtocolContract instances, got #{entries.inspect}"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# ADR-32 WD4 — `source_rbs_synthesizer:` declares a callable
|
|
399
|
+
# the engine invokes once per analysed Ruby source file at
|
|
400
|
+
# env-build time. The callable receives a source file path
|
|
401
|
+
# (String) and returns either an RBS source String to merge
|
|
402
|
+
# into the analysis environment or `nil` (no contribution
|
|
403
|
+
# for this file). Distinct from `signature_paths:` (static,
|
|
404
|
+
# bundled RBS): the synthesizer derives RBS from project
|
|
405
|
+
# source on each run.
|
|
406
|
+
#
|
|
407
|
+
# The value is held as-given (no dup / freeze) because a
|
|
408
|
+
# callable instance is opaque to the manifest; the plugin
|
|
409
|
+
# author is responsible for thread-/Ractor-safety of any
|
|
410
|
+
# captured state (per ADR-15).
|
|
411
|
+
def validate_source_rbs_synthesizer!(synthesizer)
|
|
412
|
+
return if synthesizer.nil?
|
|
413
|
+
return if synthesizer.respond_to?(:call)
|
|
414
|
+
|
|
415
|
+
raise ArgumentError,
|
|
416
|
+
"plugin manifest source_rbs_synthesizer must respond to :call, " \
|
|
417
|
+
"got #{synthesizer.inspect}"
|
|
418
|
+
end
|
|
419
|
+
|
|
329
420
|
def coerce_consumes(consumes)
|
|
330
421
|
unless consumes.is_a?(Array)
|
|
331
422
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|