rigortype 0.1.11 → 0.1.13
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/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +37 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +195 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
- data/sig/rigor/scope.rbs +23 -0
- data/skills/rigor-baseline-reduce/SKILL.md +100 -0
- data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
- data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
- data/skills/rigor-plugin-author/SKILL.md +95 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
- data/skills/rigor-project-init/SKILL.md +129 -0
- data/skills/rigor-project-init/references/01-detect.md +101 -0
- data/skills/rigor-project-init/references/02-configure.md +185 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +22 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renderer for `rigor plugins`. Produces a human-readable
|
|
8
|
+
# text table and a JSON representation from the same row
|
|
9
|
+
# shape (the Hash documented on
|
|
10
|
+
# {Rigor::CLI::PluginsCommand#loaded_row}).
|
|
11
|
+
#
|
|
12
|
+
# The two formats carry the same content; JSON is meant for
|
|
13
|
+
# tooling (SKILLs, CI, editor integrations) while text is
|
|
14
|
+
# for interactive inspection. Rows are printed in the order
|
|
15
|
+
# the loader resolved them.
|
|
16
|
+
class PluginsRenderer
|
|
17
|
+
def initialize(rows:, configuration_path:)
|
|
18
|
+
@rows = rows
|
|
19
|
+
@configuration_path = configuration_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def text
|
|
23
|
+
lines = []
|
|
24
|
+
lines << header
|
|
25
|
+
lines << ""
|
|
26
|
+
@rows.each_with_index do |row, index|
|
|
27
|
+
lines.concat(row_lines(row))
|
|
28
|
+
lines << "" unless index == @rows.size - 1
|
|
29
|
+
end
|
|
30
|
+
lines << ""
|
|
31
|
+
lines << footer
|
|
32
|
+
lines.join("\n")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def json
|
|
36
|
+
JSON.pretty_generate(
|
|
37
|
+
{
|
|
38
|
+
"configuration" => @configuration_path,
|
|
39
|
+
"plugins" => @rows.map { |row| row_json(row) },
|
|
40
|
+
"summary" => summary
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def header
|
|
48
|
+
loaded = @rows.count { |r| r[:status] == :loaded }
|
|
49
|
+
errored = @rows.count { |r| r[:status] == :load_error }
|
|
50
|
+
config = @configuration_path || "(no .rigor.yml found; using defaults)"
|
|
51
|
+
"Plugin activation report\n " \
|
|
52
|
+
"configuration: #{config}\n " \
|
|
53
|
+
"loaded: #{loaded} load-error: #{errored}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def footer
|
|
57
|
+
errored = @rows.select { |r| r[:status] == :load_error }
|
|
58
|
+
if errored.empty?
|
|
59
|
+
"All configured plugins loaded successfully."
|
|
60
|
+
else
|
|
61
|
+
"#{errored.size} plugin(s) failed to load — see above. " \
|
|
62
|
+
"Run with --strict to make this a CI gate."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def row_lines(row)
|
|
67
|
+
marker = row[:status] == :loaded ? "OK " : "ERR"
|
|
68
|
+
head = if row[:status] == :loaded
|
|
69
|
+
" [#{marker}] #{row[:id]} v#{row[:version]} (#{row[:gem]})"
|
|
70
|
+
else
|
|
71
|
+
" [#{marker}] #{row[:gem]}"
|
|
72
|
+
end
|
|
73
|
+
lines = [head]
|
|
74
|
+
lines << " #{row[:description]}" if row[:description]
|
|
75
|
+
|
|
76
|
+
if row[:status] == :load_error
|
|
77
|
+
lines << " load error: #{row[:load_error]}"
|
|
78
|
+
lines << " config: #{row[:config].inspect}" if row[:config] && !row[:config].empty?
|
|
79
|
+
return lines
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
lines.concat(loaded_detail_lines(row))
|
|
83
|
+
lines
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def loaded_detail_lines(row)
|
|
87
|
+
lines = []
|
|
88
|
+
lines.concat(signature_paths_lines(row[:signature_paths])) if row[:signature_paths].any?
|
|
89
|
+
lines.concat(receiver_and_fact_lines(row))
|
|
90
|
+
lines.concat(macro_substrate_lines(row))
|
|
91
|
+
lines.concat(extra_surface_lines(row))
|
|
92
|
+
lines << " config: #{row[:config].inspect}" if row[:config] && !row[:config].empty?
|
|
93
|
+
lines
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def receiver_and_fact_lines(row)
|
|
97
|
+
lines = []
|
|
98
|
+
lines << " open_receivers: #{row[:open_receivers].join(', ')}" if row[:open_receivers].any?
|
|
99
|
+
lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
|
|
100
|
+
lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
|
|
101
|
+
lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
|
|
102
|
+
lines
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extra_surface_lines(row)
|
|
106
|
+
lines = []
|
|
107
|
+
lines << " protocol_contracts: #{row[:protocol_contracts]}" if row[:protocol_contracts].positive?
|
|
108
|
+
lines << " source_rbs_synthesizer: yes" if row[:source_rbs_synthesizer]
|
|
109
|
+
lines << " type_node_resolvers: #{row[:type_node_resolvers]}" if row[:type_node_resolvers].positive?
|
|
110
|
+
if row[:hkt_registrations].positive? || row[:hkt_definitions].positive?
|
|
111
|
+
lines << " hkt: #{row[:hkt_registrations]} registration(s), #{row[:hkt_definitions]} definition(s)"
|
|
112
|
+
end
|
|
113
|
+
lines
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def signature_paths_lines(paths)
|
|
117
|
+
lines = [" signature_paths:"]
|
|
118
|
+
paths.each do |entry|
|
|
119
|
+
marker = entry[:exists] ? "" : " (MISSING)"
|
|
120
|
+
lines << " #{entry[:path]} (#{entry[:rbs_files]} .rbs file(s))#{marker}"
|
|
121
|
+
end
|
|
122
|
+
lines
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def macro_substrate_lines(row)
|
|
126
|
+
parts = []
|
|
127
|
+
parts << "block_as_methods=#{row[:block_as_methods]}" if row[:block_as_methods].positive?
|
|
128
|
+
parts << "heredoc_templates=#{row[:heredoc_templates]}" if row[:heredoc_templates].positive?
|
|
129
|
+
parts << "trait_registries=#{row[:trait_registries]}" if row[:trait_registries].positive?
|
|
130
|
+
parts << "external_files=#{row[:external_files]}" if row[:external_files].positive?
|
|
131
|
+
return [] if parts.empty?
|
|
132
|
+
|
|
133
|
+
[" macro substrate: #{parts.join(', ')}"]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def row_json(row)
|
|
137
|
+
{
|
|
138
|
+
"gem" => row[:gem],
|
|
139
|
+
"status" => row[:status].to_s,
|
|
140
|
+
"id" => row[:id],
|
|
141
|
+
"version" => row[:version],
|
|
142
|
+
"description" => row[:description],
|
|
143
|
+
"config" => row[:config],
|
|
144
|
+
"signature_paths" => row[:signature_paths].map do |sp|
|
|
145
|
+
{ "path" => sp[:path], "exists" => sp[:exists], "rbs_files" => sp[:rbs_files] }
|
|
146
|
+
end,
|
|
147
|
+
"open_receivers" => row[:open_receivers],
|
|
148
|
+
"owns_receivers" => row[:owns_receivers],
|
|
149
|
+
"produces" => row[:produces],
|
|
150
|
+
"consumes" => row[:consumes],
|
|
151
|
+
"block_as_methods" => row[:block_as_methods],
|
|
152
|
+
"heredoc_templates" => row[:heredoc_templates],
|
|
153
|
+
"trait_registries" => row[:trait_registries],
|
|
154
|
+
"external_files" => row[:external_files],
|
|
155
|
+
"type_node_resolvers" => row[:type_node_resolvers],
|
|
156
|
+
"hkt_registrations" => row[:hkt_registrations],
|
|
157
|
+
"hkt_definitions" => row[:hkt_definitions],
|
|
158
|
+
"protocol_contracts" => row[:protocol_contracts],
|
|
159
|
+
"source_rbs_synthesizer" => row[:source_rbs_synthesizer],
|
|
160
|
+
"load_error" => row[:load_error]
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def summary
|
|
165
|
+
{
|
|
166
|
+
"total" => @rows.size,
|
|
167
|
+
"loaded" => @rows.count { |r| r[:status] == :loaded },
|
|
168
|
+
"load_error" => @rows.count { |r| r[:status] == :load_error }
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# `rigor skill` — discover and print the SKILL.md files
|
|
8
|
+
# bundled with the `rigortype` gem.
|
|
9
|
+
#
|
|
10
|
+
# Rigor ships a small set of Agent Skills under `skills/` that
|
|
11
|
+
# walk an AI coding agent through onboarding (`rigor-project-init`),
|
|
12
|
+
# baseline reduction (`rigor-baseline-reduce`), and authoring a
|
|
13
|
+
# plugin (`rigor-plugin-author`). When Rigor is installed via
|
|
14
|
+
# `mise` / `gem install` / etc. the SKILL files live inside the
|
|
15
|
+
# gem checkout — the project being analysed has no copy, so an
|
|
16
|
+
# AI agent has no a priori way to find them.
|
|
17
|
+
#
|
|
18
|
+
# This command exposes the bundled skills via three subcommands:
|
|
19
|
+
#
|
|
20
|
+
# - `rigor skill list` — table of name + absolute path.
|
|
21
|
+
# - `rigor skill print <name>` — short header (paths + how to use)
|
|
22
|
+
# followed by the SKILL.md body. This
|
|
23
|
+
# is the form AI agents should call;
|
|
24
|
+
# the inline body plus the header's
|
|
25
|
+
# absolute paths together let the
|
|
26
|
+
# agent act with or without a file
|
|
27
|
+
# reading tool.
|
|
28
|
+
# - `rigor skill path <name>` — one-line absolute path, suitable
|
|
29
|
+
# as input to a Read tool.
|
|
30
|
+
#
|
|
31
|
+
# `rigor skill` with no subcommand is an alias for `list`.
|
|
32
|
+
class SkillCommand
|
|
33
|
+
USAGE = <<~USAGE
|
|
34
|
+
Usage: rigor skill <subcommand> [args]
|
|
35
|
+
|
|
36
|
+
Subcommands:
|
|
37
|
+
list List bundled skills (default when no subcommand given)
|
|
38
|
+
print <name> Print the SKILL.md body for <name> to stdout, with a header
|
|
39
|
+
path <name> Print the absolute path of the SKILL.md file for <name>
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
rigor skill list
|
|
43
|
+
rigor skill print rigor-project-init
|
|
44
|
+
rigor skill path rigor-baseline-reduce
|
|
45
|
+
USAGE
|
|
46
|
+
|
|
47
|
+
# The bundled skills live at `<gem_root>/skills/`. From
|
|
48
|
+
# `lib/rigor/cli/skill_command.rb` that is three directories up.
|
|
49
|
+
SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
|
|
50
|
+
|
|
51
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
52
|
+
@argv = argv
|
|
53
|
+
@out = out
|
|
54
|
+
@err = err
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer] CLI exit status.
|
|
58
|
+
def run
|
|
59
|
+
subcommand = @argv.shift || "list"
|
|
60
|
+
|
|
61
|
+
case subcommand
|
|
62
|
+
when "list" then run_list
|
|
63
|
+
when "print" then run_print
|
|
64
|
+
when "path" then run_path
|
|
65
|
+
when "-h", "--help", "help"
|
|
66
|
+
print_usage(@out)
|
|
67
|
+
0
|
|
68
|
+
else
|
|
69
|
+
@err.puts("Unknown subcommand: #{subcommand}")
|
|
70
|
+
print_usage(@err)
|
|
71
|
+
Rigor::CLI::EXIT_USAGE
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def run_list
|
|
78
|
+
skills = discover_skills
|
|
79
|
+
if skills.empty?
|
|
80
|
+
@err.puts("No bundled skills found under #{SKILLS_ROOT}")
|
|
81
|
+
return 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
width = skills.map { |s| s.fetch(:name).length }.max
|
|
85
|
+
skills.each do |skill|
|
|
86
|
+
@out.puts(format("%-#{width}s %s", skill.fetch(:name), skill.fetch(:path)))
|
|
87
|
+
end
|
|
88
|
+
0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_print
|
|
92
|
+
name = @argv.shift
|
|
93
|
+
return usage_error("`print` requires a skill name") if name.nil?
|
|
94
|
+
|
|
95
|
+
skill = find_skill(name)
|
|
96
|
+
return name_error(name) if skill.nil?
|
|
97
|
+
|
|
98
|
+
@out.puts(render_print_header(skill))
|
|
99
|
+
@out.puts
|
|
100
|
+
@out.write(File.read(skill.fetch(:path)))
|
|
101
|
+
0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run_path
|
|
105
|
+
name = @argv.shift
|
|
106
|
+
return usage_error("`path` requires a skill name") if name.nil?
|
|
107
|
+
|
|
108
|
+
skill = find_skill(name)
|
|
109
|
+
return name_error(name) if skill.nil?
|
|
110
|
+
|
|
111
|
+
@out.puts(skill.fetch(:path))
|
|
112
|
+
0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The header that precedes the SKILL.md body when an agent
|
|
116
|
+
# runs `rigor skill print <name>`. Kept as `# `-prefixed
|
|
117
|
+
# comment lines so the combined output remains parseable as
|
|
118
|
+
# markdown — anything below `---` (the SKILL frontmatter
|
|
119
|
+
# marker) is unchanged.
|
|
120
|
+
def render_print_header(skill)
|
|
121
|
+
references_dir = File.join(File.dirname(skill.fetch(:path)), "references")
|
|
122
|
+
ref_line = if File.directory?(references_dir)
|
|
123
|
+
"# References: #{references_dir}/ (read referenced `references/NN-*.md` files from here)"
|
|
124
|
+
else
|
|
125
|
+
"# References: (none)"
|
|
126
|
+
end
|
|
127
|
+
<<~HEADER.chomp
|
|
128
|
+
# Rigor skill: #{skill.fetch(:name)}
|
|
129
|
+
# Source: #{skill.fetch(:path)}
|
|
130
|
+
#{ref_line}
|
|
131
|
+
#
|
|
132
|
+
# The body below is the canonical SKILL definition shipped with
|
|
133
|
+
# rigortype #{Rigor::VERSION}. Follow its instructions.
|
|
134
|
+
HEADER
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def discover_skills
|
|
138
|
+
return [] unless File.directory?(SKILLS_ROOT)
|
|
139
|
+
|
|
140
|
+
Dir.children(SKILLS_ROOT).sort.filter_map do |name|
|
|
141
|
+
skill_md = File.join(SKILLS_ROOT, name, "SKILL.md")
|
|
142
|
+
next unless File.file?(skill_md)
|
|
143
|
+
|
|
144
|
+
{ name: name, path: skill_md }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def find_skill(name)
|
|
149
|
+
discover_skills.find { |s| s.fetch(:name) == name }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def name_error(name)
|
|
153
|
+
@err.puts("Unknown skill: #{name}")
|
|
154
|
+
@err.puts("Available skills:")
|
|
155
|
+
discover_skills.each { |s| @err.puts(" #{s.fetch(:name)}") }
|
|
156
|
+
1
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def usage_error(message)
|
|
160
|
+
@err.puts(message)
|
|
161
|
+
print_usage(@err)
|
|
162
|
+
Rigor::CLI::EXIT_USAGE
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def print_usage(io)
|
|
166
|
+
io.puts(USAGE)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -32,7 +32,9 @@ module Rigor
|
|
|
32
32
|
"baseline" => :run_baseline,
|
|
33
33
|
"triage" => :run_triage,
|
|
34
34
|
"coverage" => :run_coverage,
|
|
35
|
-
"
|
|
35
|
+
"plugins" => :run_plugins,
|
|
36
|
+
"playground" => :run_playground,
|
|
37
|
+
"skill" => :run_skill
|
|
36
38
|
}.freeze
|
|
37
39
|
|
|
38
40
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -475,9 +477,29 @@ module Rigor
|
|
|
475
477
|
|
|
476
478
|
File.write(path, init_template)
|
|
477
479
|
@out.puts("Created #{path}")
|
|
480
|
+
print_init_next_steps(path)
|
|
478
481
|
0
|
|
479
482
|
end
|
|
480
483
|
|
|
484
|
+
# `rigor init`'s template ships empty `plugins:` so a fresh
|
|
485
|
+
# init has nothing to validate — but the moment the user adds
|
|
486
|
+
# any plugin entry, the activation-failure surfaces enumerated
|
|
487
|
+
# in `rigor plugins`'s docstring become real. Point them at
|
|
488
|
+
# the verification command + the canonical readiness flow so
|
|
489
|
+
# silent failures (the cwd / Gemfile / signature_paths
|
|
490
|
+
# mismatches that surfaced during the Mastodon trial) get
|
|
491
|
+
# caught the first time the user wires a plugin, not the first
|
|
492
|
+
# time `rigor check` reports false positives that should have
|
|
493
|
+
# been covered.
|
|
494
|
+
def print_init_next_steps(path)
|
|
495
|
+
@out.puts ""
|
|
496
|
+
@out.puts "Next steps:"
|
|
497
|
+
@out.puts " 1. Edit #{path} — add the `plugins:` your project needs."
|
|
498
|
+
@out.puts " 2. Run `rigor plugins` to verify every configured plugin loads."
|
|
499
|
+
@out.puts " (`--strict` exits 1 on failure; ideal CI gate.)"
|
|
500
|
+
@out.puts " 3. Run `rigor check` to analyse your code."
|
|
501
|
+
end
|
|
502
|
+
|
|
481
503
|
# Renders the starter `.rigor.yml` body. The template
|
|
482
504
|
# serialises `Configuration::DEFAULTS` (so the on-disk file
|
|
483
505
|
# round-trips through `Configuration.load`) and prepends a
|
|
@@ -597,6 +619,12 @@ module Rigor
|
|
|
597
619
|
CLI::CoverageCommand.new(argv: @argv, out: @out, err: @err).run
|
|
598
620
|
end
|
|
599
621
|
|
|
622
|
+
def run_plugins
|
|
623
|
+
require_relative "cli/plugins_command"
|
|
624
|
+
|
|
625
|
+
CLI::PluginsCommand.new(argv: @argv, out: @out, err: @err).run
|
|
626
|
+
end
|
|
627
|
+
|
|
600
628
|
def run_playground
|
|
601
629
|
begin
|
|
602
630
|
require "rigor/playground"
|
|
@@ -608,6 +636,12 @@ module Rigor
|
|
|
608
636
|
Rigor::CLI::PlaygroundCommand.new(@argv[1..], @out, @err).run
|
|
609
637
|
end
|
|
610
638
|
|
|
639
|
+
def run_skill
|
|
640
|
+
require_relative "cli/skill_command"
|
|
641
|
+
|
|
642
|
+
CLI::SkillCommand.new(argv: @argv, out: @out, err: @err).run
|
|
643
|
+
end
|
|
644
|
+
|
|
611
645
|
def write_result(result, format)
|
|
612
646
|
case format
|
|
613
647
|
when "json"
|
|
@@ -653,7 +687,9 @@ module Rigor
|
|
|
653
687
|
mcp Run the Rigor MCP server over stdio (ADR-33)
|
|
654
688
|
triage Summarise diagnostics: distribution, hotspots, hints (ADR-23)
|
|
655
689
|
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
690
|
+
plugins Report activation status of every configured plugin
|
|
656
691
|
playground Start the browser playground (requires rigor-playground gem)
|
|
692
|
+
skill List or print bundled Agent Skills (rigor-project-init, ...)
|
|
657
693
|
version Print the Rigor version
|
|
658
694
|
help Print this help
|
|
659
695
|
HELP
|
|
@@ -39,6 +39,7 @@ module Rigor
|
|
|
39
39
|
PROFILES = {
|
|
40
40
|
lenient: {
|
|
41
41
|
"call.undefined-method" => :error,
|
|
42
|
+
"call.unresolved-toplevel" => :off,
|
|
42
43
|
"call.wrong-arity" => :error,
|
|
43
44
|
"call.argument-type-mismatch" => :warning,
|
|
44
45
|
"call.possible-nil-receiver" => :warning,
|
|
@@ -54,6 +55,7 @@ module Rigor
|
|
|
54
55
|
}.freeze,
|
|
55
56
|
balanced: {
|
|
56
57
|
"call.undefined-method" => :error,
|
|
58
|
+
"call.unresolved-toplevel" => :warning,
|
|
57
59
|
"call.wrong-arity" => :error,
|
|
58
60
|
"call.argument-type-mismatch" => :error,
|
|
59
61
|
"call.possible-nil-receiver" => :error,
|
|
@@ -69,6 +71,7 @@ module Rigor
|
|
|
69
71
|
}.freeze,
|
|
70
72
|
strict: {
|
|
71
73
|
"call.undefined-method" => :error,
|
|
74
|
+
"call.unresolved-toplevel" => :error,
|
|
72
75
|
"call.wrong-arity" => :error,
|
|
73
76
|
"call.argument-type-mismatch" => :error,
|
|
74
77
|
"call.possible-nil-receiver" => :error,
|
|
@@ -106,6 +106,8 @@ module Rigor
|
|
|
106
106
|
params_node = params_root.parameters
|
|
107
107
|
return {} if params_node.nil?
|
|
108
108
|
|
|
109
|
+
apply_auto_splat(params_node)
|
|
110
|
+
|
|
109
111
|
bindings = {}
|
|
110
112
|
bind_positionals(params_node, bindings, 0)
|
|
111
113
|
bind_rest(params_node, bindings)
|
|
@@ -115,6 +117,39 @@ module Rigor
|
|
|
115
117
|
bindings
|
|
116
118
|
end
|
|
117
119
|
|
|
120
|
+
# Ruby blocks (NOT lambdas) auto-splat a single yielded
|
|
121
|
+
# Tuple-shaped value when the block declares more than one
|
|
122
|
+
# required positional parameter:
|
|
123
|
+
#
|
|
124
|
+
# { a: 1 }.each { |k, v| ... }
|
|
125
|
+
#
|
|
126
|
+
# yields `[key, value]` as a single arg, but the two-param
|
|
127
|
+
# block sees `k = key, v = value`. RBS / IteratorDispatch
|
|
128
|
+
# encode this as the block taking ONE `[K, V]` Tuple
|
|
129
|
+
# parameter; without this fix-up the binder would assign
|
|
130
|
+
# `k = Tuple[K, V]` and `v = Dynamic[Top]`, and any call
|
|
131
|
+
# on `k.<method-not-on-Tuple>` would false-fire.
|
|
132
|
+
#
|
|
133
|
+
# The rule fires only when (a) the receiver yields exactly
|
|
134
|
+
# one value (`expected_param_types.size == 1`), (b) the
|
|
135
|
+
# block declares more than one positional slot, and (c)
|
|
136
|
+
# that single expected element is a Tuple. Multi-arg yields
|
|
137
|
+
# (e.g. `each_with_index`'s `(element, index)` pair) are
|
|
138
|
+
# NOT auto-splatted — matching Ruby semantics where a
|
|
139
|
+
# multi-arg yield to a `|a, b, c|` block fills the extra
|
|
140
|
+
# slot with nil rather than splatting any element.
|
|
141
|
+
def apply_auto_splat(params_node)
|
|
142
|
+
return unless @expected_param_types.size == 1
|
|
143
|
+
|
|
144
|
+
pos_count = params_node.requireds.size + params_node.optionals.size + params_node.posts.size
|
|
145
|
+
return unless pos_count > 1
|
|
146
|
+
|
|
147
|
+
first = @expected_param_types[0]
|
|
148
|
+
return unless first.is_a?(Type::Tuple)
|
|
149
|
+
|
|
150
|
+
@expected_param_types = first.elements
|
|
151
|
+
end
|
|
152
|
+
|
|
118
153
|
def bind_positionals(params_node, bindings, cursor)
|
|
119
154
|
cursor = bind_required_positionals(params_node, bindings, cursor)
|
|
120
155
|
cursor = bind_optional_positionals(params_node, bindings, cursor)
|
|
@@ -6,6 +6,7 @@ require_relative "../type"
|
|
|
6
6
|
require_relative "../ast"
|
|
7
7
|
require_relative "block_parameter_binder"
|
|
8
8
|
require_relative "fallback"
|
|
9
|
+
require_relative "indexed_narrowing"
|
|
9
10
|
require_relative "macro_block_self_type"
|
|
10
11
|
require_relative "method_dispatcher"
|
|
11
12
|
require_relative "narrowing"
|
|
@@ -1133,6 +1134,69 @@ module Rigor
|
|
|
1133
1134
|
type_of(body.last)
|
|
1134
1135
|
end
|
|
1135
1136
|
|
|
1137
|
+
# Indexed-collection narrowing — `receiver[key]` after a
|
|
1138
|
+
# prior `receiver[key] ||= default` reads the post-`||=`
|
|
1139
|
+
# type when the receiver and key are stable enough to
|
|
1140
|
+
# address. Sits ahead of `MethodDispatcher.dispatch` so
|
|
1141
|
+
# the standard `Hash#[]` / `Array#[]` answer (which would
|
|
1142
|
+
# fold to `Constant[nil]` for an empty `HashShape{}` or
|
|
1143
|
+
# `Tuple[]`) does not override the narrowing. See
|
|
1144
|
+
# {Inference::IndexedNarrowing}.
|
|
1145
|
+
def indexed_narrowing_for(node)
|
|
1146
|
+
IndexedNarrowing.lookup_for_call(node, scope) || method_chain_narrowing_for(node)
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
# Stable single-hop chain narrowing — `receiver.method`
|
|
1150
|
+
# after an `is_a?` / `kind_of?` / `instance_of?` predicate
|
|
1151
|
+
# established the narrowing on the dominated edge. The
|
|
1152
|
+
# call MUST be no-arg + no-block + rooted at a local-var /
|
|
1153
|
+
# ivar read; everything else falls through to the
|
|
1154
|
+
# standard dispatcher. ROADMAP § Future cycles —
|
|
1155
|
+
# "Method-call receiver narrowing across stable
|
|
1156
|
+
# receivers" — Law-of-Demeter-justified single-hop scope.
|
|
1157
|
+
def method_chain_narrowing_for(node)
|
|
1158
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
1159
|
+
return nil unless node.block.nil?
|
|
1160
|
+
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
1161
|
+
|
|
1162
|
+
case node.receiver
|
|
1163
|
+
when Prism::LocalVariableReadNode
|
|
1164
|
+
scope.method_chain_narrowing(:local, node.receiver.name, node.name)
|
|
1165
|
+
when Prism::InstanceVariableReadNode
|
|
1166
|
+
scope.method_chain_narrowing(:ivar, node.receiver.name, node.name)
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# v0.0.3 A — implicit-self calls prefer a same-named
|
|
1171
|
+
# top-level `def` over RBS dispatch. Without this,
|
|
1172
|
+
# a helper like `def select(...)` defined inside an
|
|
1173
|
+
# `RSpec.describe ... do ... end` block mis-routes
|
|
1174
|
+
# through `Enumerable#select` / `Object#select` and
|
|
1175
|
+
# the caller observes `Array[Elem]` instead of the
|
|
1176
|
+
# helper's actual return type. The check fires only
|
|
1177
|
+
# for `node.receiver.nil?` (true implicit self), so
|
|
1178
|
+
# explicit-receiver dispatch is unaffected.
|
|
1179
|
+
def try_local_def_dispatch(node, receiver, arg_types)
|
|
1180
|
+
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1181
|
+
return nil unless local_def
|
|
1182
|
+
|
|
1183
|
+
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1184
|
+
return local_inference if local_inference && adoptable_self_call_result?(local_inference)
|
|
1185
|
+
|
|
1186
|
+
# The local def matches by name but the inference was
|
|
1187
|
+
# disqualified — either the parameter shape is too complex
|
|
1188
|
+
# for the first-iteration binder (kwargs / optionals /
|
|
1189
|
+
# rest), or ADR-24 slice 1's conservative gate declined
|
|
1190
|
+
# the resolved return type inside a class body (see
|
|
1191
|
+
# `adoptable_self_call_result?`). `Dynamic[Top]` is the
|
|
1192
|
+
# safest answer: RBS dispatch would be wrong (the method
|
|
1193
|
+
# is user-defined and shadows whatever ancestor method the
|
|
1194
|
+
# dispatch would find), and `Dynamic[Top]` propagates
|
|
1195
|
+
# correctly through downstream call chains without
|
|
1196
|
+
# surfacing misleading false-positive diagnostics.
|
|
1197
|
+
dynamic_top
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1136
1200
|
# Slice 2 routes call expressions through `MethodDispatcher`. The
|
|
1137
1201
|
# receiver and every argument are typed first, then the dispatcher is
|
|
1138
1202
|
# asked for a result type. A nil result triggers the fail-soft fallback
|
|
@@ -1140,40 +1204,15 @@ module Rigor
|
|
|
1140
1204
|
# their own fallbacks for unrecognised receivers/args, so the tracer
|
|
1141
1205
|
# captures both the immediate dispatch miss and the deeper cause).
|
|
1142
1206
|
def call_type_for(node)
|
|
1207
|
+
narrowed = indexed_narrowing_for(node)
|
|
1208
|
+
return narrowed if narrowed
|
|
1209
|
+
|
|
1143
1210
|
receiver = call_receiver_type_for(node)
|
|
1144
1211
|
arg_types = call_arg_types(node)
|
|
1145
1212
|
block_type = block_return_type_for(node, receiver, arg_types)
|
|
1146
1213
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
# a helper like `def select(...)` defined inside an
|
|
1150
|
-
# `RSpec.describe ... do ... end` block mis-routes
|
|
1151
|
-
# through `Enumerable#select` / `Object#select` and
|
|
1152
|
-
# the caller observes `Array[Elem]` instead of the
|
|
1153
|
-
# helper's actual return type. The check fires only
|
|
1154
|
-
# for `node.receiver.nil?` (true implicit self), so
|
|
1155
|
-
# explicit-receiver dispatch is unaffected.
|
|
1156
|
-
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1157
|
-
if local_def
|
|
1158
|
-
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1159
|
-
return local_inference if local_inference && adoptable_self_call_result?(local_inference)
|
|
1160
|
-
|
|
1161
|
-
# The local def matches by name but the inference
|
|
1162
|
-
# was disqualified — either the parameter shape is
|
|
1163
|
-
# too complex for the first-iteration binder
|
|
1164
|
-
# (kwargs / optionals / rest), or ADR-24 slice 1's
|
|
1165
|
-
# conservative gate declined the resolved return
|
|
1166
|
-
# type inside a class body (see
|
|
1167
|
-
# `adoptable_self_call_result?`).
|
|
1168
|
-
# Returning `Dynamic[Top]` is the safest answer:
|
|
1169
|
-
# we know RBS dispatch would be wrong (the
|
|
1170
|
-
# method is user-defined and shadows whatever
|
|
1171
|
-
# ancestor method the dispatch would find), and
|
|
1172
|
-
# `Dynamic[Top]` propagates correctly through
|
|
1173
|
-
# downstream call chains without surfacing
|
|
1174
|
-
# misleading false-positive diagnostics.
|
|
1175
|
-
return dynamic_top
|
|
1176
|
-
end
|
|
1214
|
+
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1215
|
+
return local_def_result if local_def_result
|
|
1177
1216
|
|
|
1178
1217
|
# v0.0.6 phase 2 — per-element block fold for Tuple
|
|
1179
1218
|
# receivers. When `[a, b, c].map { |x| f(x) }` and the
|