rigortype 0.1.12 → 0.1.14
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 +12 -2
- data/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +29 -7
- data/lib/rigor/environment/rbs_loader.rb +17 -0
- data/lib/rigor/environment.rb +13 -3
- data/lib/rigor/scope.rb +14 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +1 -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 +139 -0
- data/skills/rigor-project-init/references/02-configure.md +202 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +169 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1dbcd9b168a06cc8d5f26e0a096f19496c3cebdc8864fb56d966741b8a42577
|
|
4
|
+
data.tar.gz: 39e428c763e8d8cac6d9743d40d5d654bfaec56362d41e338a6dac974dff27cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cd6a4d0b52fcd2e1e2bacddd6120154ccbe075ef86615c38d7e6d1d942ea158f55abaeb03f01c0a23315a187076912315af98ce74faa4c174c4f6c5c98eac74
|
|
7
|
+
data.tar.gz: 1d7b600e3dd97a38bf1ac08231a7aa07635210723adf349c014d9100ccc2b0a868deb8a6479d99207e1c0b5e4cf07c158f0f0cb3bda325ec5c7d143e4f5ae90b
|
data/README.md
CHANGED
|
@@ -49,8 +49,18 @@ Rigor is a tool, not a library — install it independently, **not** in
|
|
|
49
49
|
your project's `Gemfile`. It runs on Ruby 4.0, regardless of which Ruby
|
|
50
50
|
version your project targets.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
**Using an AI coding agent?** Hand it this prompt and it will detect
|
|
53
|
+
your environment, install Rigor, and kick off the project-init Skill
|
|
54
|
+
automatically:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Install Rigor in this project by following the instructions at
|
|
58
|
+
https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Manual install** — the recommended path uses
|
|
62
|
+
[`mise`](https://mise.jdx.dev/), which provisions both Ruby 4.0 and
|
|
63
|
+
Rigor pinned per project:
|
|
54
64
|
|
|
55
65
|
```sh
|
|
56
66
|
mise use ruby@4.0
|
|
@@ -57,6 +57,7 @@ module Rigor
|
|
|
57
57
|
# system; new rules MUST register here so user configuration
|
|
58
58
|
# can refer to them.
|
|
59
59
|
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
60
|
+
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
60
61
|
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
61
62
|
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
62
63
|
RULE_NIL_RECEIVER = "call.possible-nil-receiver"
|
|
@@ -72,6 +73,7 @@ module Rigor
|
|
|
72
73
|
|
|
73
74
|
ALL_RULES = [
|
|
74
75
|
RULE_UNDEFINED_METHOD,
|
|
76
|
+
RULE_UNRESOLVED_TOPLEVEL,
|
|
75
77
|
RULE_WRONG_ARITY,
|
|
76
78
|
RULE_ARGUMENT_TYPE,
|
|
77
79
|
RULE_NIL_RECEIVER,
|
|
@@ -162,6 +164,7 @@ module Rigor
|
|
|
162
164
|
def call_node_diagnostics(path, node, scope_index)
|
|
163
165
|
[
|
|
164
166
|
undefined_method_diagnostic(path, node, scope_index),
|
|
167
|
+
unresolved_toplevel_diagnostic(path, node, scope_index),
|
|
165
168
|
wrong_arity_diagnostic(path, node, scope_index),
|
|
166
169
|
argument_type_diagnostic(path, node, scope_index),
|
|
167
170
|
nil_receiver_diagnostic(path, node, scope_index),
|
|
@@ -365,10 +368,14 @@ module Rigor
|
|
|
365
368
|
return nil if open_receiver?(class_name, scope)
|
|
366
369
|
|
|
367
370
|
# Slice 7 phase 12 — suppress when the user has
|
|
368
|
-
# declared the method in source (
|
|
369
|
-
# `
|
|
371
|
+
# declared the method in source (`def` /
|
|
372
|
+
# `define_method`) OR in a `pre_eval:` monkey-patch
|
|
373
|
+
# file (ADR-17). Both paths are project-side method
|
|
374
|
+
# contributions the dispatcher already resolved; the
|
|
375
|
+
# rule must not surface a false `undefined-method`
|
|
376
|
+
# for them.
|
|
370
377
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
371
|
-
return nil if
|
|
378
|
+
return nil if source_declared_method?(scope, class_name, call_node.name, kind)
|
|
372
379
|
|
|
373
380
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
374
381
|
|
|
@@ -404,6 +411,92 @@ module Rigor
|
|
|
404
411
|
scope.environment.rbs_module?(receiver_type.class_name)
|
|
405
412
|
end
|
|
406
413
|
|
|
414
|
+
# Combined suppression probe for `undefined-method` /
|
|
415
|
+
# `unresolved-toplevel`. Returns true when the method is
|
|
416
|
+
# declared by any project-side contributor the dispatcher
|
|
417
|
+
# already resolves: an in-source `def` / `define_method`
|
|
418
|
+
# (`scope.discovered_method?`) OR an ADR-17 `pre_eval:`
|
|
419
|
+
# monkey-patch (`Environment#project_patched_methods`).
|
|
420
|
+
# Both paths sit at the same dispatcher precedence; the
|
|
421
|
+
# check must hold them together so neither rule fires a
|
|
422
|
+
# false positive.
|
|
423
|
+
def source_declared_method?(scope, class_name, method_name, kind)
|
|
424
|
+
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
425
|
+
|
|
426
|
+
project_patched_method?(scope, class_name, method_name, kind)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# ADR-17 § "Inference contract" — consults
|
|
430
|
+
# `Environment#project_patched_methods` so a `def` declared
|
|
431
|
+
# in a `pre_eval:` file suppresses the diagnostic at the
|
|
432
|
+
# same dispatcher precedence the registry holds for type
|
|
433
|
+
# inference (between plugins and dependency-source).
|
|
434
|
+
# Returns false when the environment carries no registry
|
|
435
|
+
# (legacy path) or the lookup misses.
|
|
436
|
+
def project_patched_method?(scope, class_name, method_name, kind)
|
|
437
|
+
environment = scope.environment
|
|
438
|
+
registry = environment&.project_patched_methods
|
|
439
|
+
return false if registry.nil? || registry.empty?
|
|
440
|
+
|
|
441
|
+
!registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind).nil?
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# ADR-34 — `call.unresolved-toplevel`. Fires on an
|
|
445
|
+
# implicit-self call (no explicit receiver) at toplevel
|
|
446
|
+
# scope (`scope.toplevel?`, i.e. outside any class /
|
|
447
|
+
# module body) whose name does not resolve against:
|
|
448
|
+
#
|
|
449
|
+
# 1. A same-file toplevel `def` via
|
|
450
|
+
# {Scope#top_level_def_for}.
|
|
451
|
+
# 2. The ADR-17 `ProjectPatchedMethods` registry under
|
|
452
|
+
# `(Object, name, :instance)` — projects declare
|
|
453
|
+
# their toplevel-injecting monkey-patches in
|
|
454
|
+
# `.rigor.yml`'s `pre_eval:` array as the canonical
|
|
455
|
+
# opt-out per ADR-34 WD2.
|
|
456
|
+
# 3. The standard `Kernel` / `Object` private-method
|
|
457
|
+
# surface (`puts`, `p`, `require`, `loop`, `raise`,
|
|
458
|
+
# …) drawn from the loaded RBS environment.
|
|
459
|
+
#
|
|
460
|
+
# The rule deliberately does NOT generalise to
|
|
461
|
+
# implicit-self calls inside `def` / `class` / `module`
|
|
462
|
+
# bodies — ADR-24 WD3's lenient-on-unresolved default
|
|
463
|
+
# stays in force there. ADR-24 WD4's gated class-body
|
|
464
|
+
# diagnostic is a separate decision this ADR does not
|
|
465
|
+
# open.
|
|
466
|
+
#
|
|
467
|
+
# Authored severity is `:warning`; the severity profile
|
|
468
|
+
# remaps it (`strict` → `:error`, `balanced` →
|
|
469
|
+
# `:warning`, `lenient` → `:off` / suppressed).
|
|
470
|
+
def unresolved_toplevel_diagnostic(path, call_node, scope_index)
|
|
471
|
+
return nil unless call_node.receiver.nil?
|
|
472
|
+
|
|
473
|
+
scope = scope_index[call_node]
|
|
474
|
+
return nil if scope.nil?
|
|
475
|
+
return nil unless scope.toplevel?
|
|
476
|
+
|
|
477
|
+
name = call_node.name
|
|
478
|
+
return nil if scope.top_level_def_for(name)
|
|
479
|
+
return nil if source_declared_method?(scope, "Object", name, :instance)
|
|
480
|
+
return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
|
|
481
|
+
|
|
482
|
+
build_unresolved_toplevel_diagnostic(path, call_node)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def build_unresolved_toplevel_diagnostic(path, call_node)
|
|
486
|
+
location = call_node.message_loc || call_node.location
|
|
487
|
+
Diagnostic.new(
|
|
488
|
+
path: path,
|
|
489
|
+
line: location.start_line,
|
|
490
|
+
column: location.start_column + 1,
|
|
491
|
+
message: "unresolved toplevel call to `#{call_node.name}`. " \
|
|
492
|
+
"If a project file defines `#{call_node.name}` via a toplevel " \
|
|
493
|
+
"`def` or a monkey-patch on Object/Kernel, list that file in " \
|
|
494
|
+
"`.rigor.yml`'s `pre_eval:` (ADR-17) so the analyzer sees it.",
|
|
495
|
+
severity: :warning,
|
|
496
|
+
rule: RULE_UNRESOLVED_TOPLEVEL
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
407
500
|
# Returns a qualified class name for the in-scope check.
|
|
408
501
|
# Nominal / Singleton carry a single-class identity
|
|
409
502
|
# directly. Constant projects to its value's class.
|
|
@@ -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
|
@@ -33,7 +33,8 @@ module Rigor
|
|
|
33
33
|
"triage" => :run_triage,
|
|
34
34
|
"coverage" => :run_coverage,
|
|
35
35
|
"plugins" => :run_plugins,
|
|
36
|
-
"playground" => :run_playground
|
|
36
|
+
"playground" => :run_playground,
|
|
37
|
+
"skill" => :run_skill
|
|
37
38
|
}.freeze
|
|
38
39
|
|
|
39
40
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -635,6 +636,12 @@ module Rigor
|
|
|
635
636
|
Rigor::CLI::PlaygroundCommand.new(@argv[1..], @out, @err).run
|
|
636
637
|
end
|
|
637
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
|
+
|
|
638
645
|
def write_result(result, format)
|
|
639
646
|
case format
|
|
640
647
|
when "json"
|
|
@@ -682,6 +689,7 @@ module Rigor
|
|
|
682
689
|
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
683
690
|
plugins Report activation status of every configured plugin
|
|
684
691
|
playground Start the browser playground (requires rigor-playground gem)
|
|
692
|
+
skill List or print bundled Agent Skills (rigor-project-init, ...)
|
|
685
693
|
version Print the Rigor version
|
|
686
694
|
help Print this help
|
|
687
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,
|
|
@@ -34,6 +34,20 @@ module Rigor
|
|
|
34
34
|
# `sig/`), and `local` (a user-managed RBS dir) — all
|
|
35
35
|
# produce a directory under the collection root and are
|
|
36
36
|
# admitted.
|
|
37
|
+
#
|
|
38
|
+
# The `source.type` filter alone is NOT sufficient: gems
|
|
39
|
+
# that were extracted from Ruby's stdlib into standalone
|
|
40
|
+
# default gems (e.g. `cgi`, `logger`, `base64`, `csv`,
|
|
41
|
+
# `bigdecimal`) are published in `ruby/gem_rbs_collection`
|
|
42
|
+
# under a `git` source type, yet rigor ALSO loads them
|
|
43
|
+
# from its bundled stdlib via `DEFAULT_LIBRARIES`. Loading
|
|
44
|
+
# both copies triggers the very
|
|
45
|
+
# `RBS::DuplicatedDeclarationError` this module exists to
|
|
46
|
+
# avoid (observed on a Rails 8 app: `.gem_rbs_collection/
|
|
47
|
+
# cgi/0.5/` vs the bundled `stdlib/cgi`). The
|
|
48
|
+
# `skip_gem_names:` parameter lets the caller pass the set
|
|
49
|
+
# of library names rigor already loads so those gems are
|
|
50
|
+
# dropped regardless of `source.type`.
|
|
37
51
|
SKIPPED_SOURCE_TYPES = Set["stdlib"].freeze
|
|
38
52
|
|
|
39
53
|
DEFAULT_COLLECTION_PATH = ".gem_rbs_collection"
|
|
@@ -47,14 +61,21 @@ module Rigor
|
|
|
47
61
|
# @param auto_detect [Boolean] when true and
|
|
48
62
|
# `lockfile_path:` is nil, look for
|
|
49
63
|
# `<project_root>/rbs_collection.lock.yaml`.
|
|
64
|
+
# @param skip_gem_names [Array<String>, Set<String>] gem
|
|
65
|
+
# names rigor already loads from its bundled stdlib (the
|
|
66
|
+
# merged `DEFAULT_LIBRARIES + libraries:` set). Entries
|
|
67
|
+
# whose `name` is in this set are dropped regardless of
|
|
68
|
+
# `source.type` to avoid `RBS::DuplicatedDeclarationError`
|
|
69
|
+
# on stdlib-extracted default gems. Defaults to empty.
|
|
50
70
|
# @return [Array<Pathname>] every
|
|
51
71
|
# `<collection_path>/<gem-name>/<gem-version>/`
|
|
52
72
|
# directory listed in the lockfile whose entry has a
|
|
53
|
-
# non-skipped source type
|
|
54
|
-
#
|
|
55
|
-
# when
|
|
56
|
-
#
|
|
57
|
-
|
|
73
|
+
# non-skipped source type, whose `name` is not in
|
|
74
|
+
# `skip_gem_names:`, and whose directory exists on disk.
|
|
75
|
+
# Returns `[]` when no lockfile is resolvable, when the
|
|
76
|
+
# YAML is unreadable, or when the collection path
|
|
77
|
+
# doesn't exist.
|
|
78
|
+
def self.discover(lockfile_path:, project_root: Dir.pwd, auto_detect: true, skip_gem_names: [])
|
|
58
79
|
resolved = resolve_lockfile_path(
|
|
59
80
|
lockfile_path: lockfile_path,
|
|
60
81
|
project_root: project_root,
|
|
@@ -68,7 +89,7 @@ module Rigor
|
|
|
68
89
|
collection_root = resolve_collection_root(resolved, data)
|
|
69
90
|
return [] unless collection_root.directory?
|
|
70
91
|
|
|
71
|
-
gem_paths_from(collection_root, data)
|
|
92
|
+
gem_paths_from(collection_root, data, skip_gem_names.to_set)
|
|
72
93
|
end
|
|
73
94
|
|
|
74
95
|
# Returns the resolved lockfile path (`Pathname`) or `nil`
|
|
@@ -105,7 +126,7 @@ module Rigor
|
|
|
105
126
|
end
|
|
106
127
|
private_class_method :resolve_collection_root
|
|
107
128
|
|
|
108
|
-
def self.gem_paths_from(collection_root, data)
|
|
129
|
+
def self.gem_paths_from(collection_root, data, skip_gem_names)
|
|
109
130
|
Array(data["gems"]).filter_map do |entry|
|
|
110
131
|
next unless entry.is_a?(Hash)
|
|
111
132
|
|
|
@@ -115,6 +136,7 @@ module Rigor
|
|
|
115
136
|
name = entry["name"]
|
|
116
137
|
version = entry["version"]
|
|
117
138
|
next if name.nil? || version.nil?
|
|
139
|
+
next if skip_gem_names.include?(name.to_s)
|
|
118
140
|
|
|
119
141
|
gem_root = collection_root + name.to_s + version.to_s
|
|
120
142
|
gem_root if gem_root.directory?
|
|
@@ -122,6 +122,23 @@ module Rigor
|
|
|
122
122
|
Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir))
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
|
+
|
|
126
|
+
# Gem names whose RBS ships under
|
|
127
|
+
# `data/vendored_gem_sigs/<gem>/`. The directory walk is
|
|
128
|
+
# the source of truth (the `README.md` sibling is not a
|
|
129
|
+
# gem and is excluded). Callers building the RBS env use
|
|
130
|
+
# this set to drop the matching `rbs collection install`
|
|
131
|
+
# directory before it double-declares against the
|
|
132
|
+
# vendored copy — the same hazard `DEFAULT_LIBRARIES`
|
|
133
|
+
# creates for stdlib-extracted gems. See
|
|
134
|
+
# `RbsCollectionDiscovery`'s `skip_gem_names:`.
|
|
135
|
+
def vendored_gem_names
|
|
136
|
+
return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
|
|
137
|
+
|
|
138
|
+
Dir.children(VENDORED_GEM_SIGS_ROOT).reject do |child|
|
|
139
|
+
File.file?(File.join(VENDORED_GEM_SIGS_ROOT, child))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
125
142
|
end
|
|
126
143
|
|
|
127
144
|
attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -263,11 +263,22 @@ module Rigor
|
|
|
263
263
|
# resulting `rbs_collection.lock.yaml` and feed each
|
|
264
264
|
# gem's `<collection_path>/<name>/<version>/` directory
|
|
265
265
|
# into `signature_paths:`. Stdlib-typed entries are
|
|
266
|
-
# skipped (already covered by `DEFAULT_LIBRARIES`)
|
|
266
|
+
# skipped (already covered by `DEFAULT_LIBRARIES`), as
|
|
267
|
+
# are gems whose RBS rigor already loads from another
|
|
268
|
+
# source — stdlib-extracted default gems (`cgi`,
|
|
269
|
+
# `logger`, …, shipped by the collection under a `git`
|
|
270
|
+
# source) and the `data/vendored_gem_sigs/` bundle
|
|
271
|
+
# (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:`
|
|
272
|
+
# passes both sets so the collection copy doesn't
|
|
273
|
+
# double-declare against rigor's bundled RBS (the
|
|
274
|
+
# `RBS::DuplicatedDeclarationError` hazard).
|
|
275
|
+
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
276
|
+
skip_gem_names = merged_libraries + RbsLoader.vendored_gem_names
|
|
267
277
|
collection_paths = RbsCollectionDiscovery.discover(
|
|
268
278
|
lockfile_path: rbs_collection_lockfile,
|
|
269
279
|
project_root: root,
|
|
270
|
-
auto_detect: rbs_collection_auto_detect
|
|
280
|
+
auto_detect: rbs_collection_auto_detect,
|
|
281
|
+
skip_gem_names: skip_gem_names
|
|
271
282
|
).map(&:to_s)
|
|
272
283
|
# ADR-25 — RBS signature directories contributed by loaded
|
|
273
284
|
# plugins via their manifest `signature_paths:`. Resolved
|
|
@@ -278,7 +289,6 @@ module Rigor
|
|
|
278
289
|
# degrades through the same O7 failure-memo path.
|
|
279
290
|
plugin_sig_paths = plugin_registry ? plugin_registry.signature_paths.map(&:to_s) : []
|
|
280
291
|
loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths
|
|
281
|
-
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
282
292
|
# ADR-32 WD4 + WD5 — invoke each loaded plugin's
|
|
283
293
|
# `source_rbs_synthesizer` once per project source file
|
|
284
294
|
# and collect non-nil `[filename, rbs_source]` pairs.
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -309,6 +309,20 @@ module Rigor
|
|
|
309
309
|
table[method_name.to_sym] == kind
|
|
310
310
|
end
|
|
311
311
|
|
|
312
|
+
# ADR-34 § "Decision" — predicate identifying a toplevel-shaped
|
|
313
|
+
# scope (no enclosing `class` / `module` body). True at the top
|
|
314
|
+
# of a file AND inside a top-level `def` body (since toplevel
|
|
315
|
+
# defs leave `self_type` nil per the existing scope-construction
|
|
316
|
+
# contract, mirroring how ADR-24's `adoptable_self_call_result?`
|
|
317
|
+
# also keys on `self_type.nil?` for the same context). Used by
|
|
318
|
+
# `CheckRules#unresolved_toplevel_diagnostic` to gate the
|
|
319
|
+
# `call.unresolved-toplevel` rule so it fires only outside
|
|
320
|
+
# class / module bodies, where Rails-DSL metaprogramming
|
|
321
|
+
# leniency (ADR-24 WD3 → WD4) does not apply.
|
|
322
|
+
def toplevel?
|
|
323
|
+
@self_type.nil?
|
|
324
|
+
end
|
|
325
|
+
|
|
312
326
|
def with_discovered_methods(table)
|
|
313
327
|
rebuild(discovered_methods: table)
|
|
314
328
|
end
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/environment.rbs
CHANGED
|
@@ -51,6 +51,7 @@ module Rigor
|
|
|
51
51
|
def self.reset_default!: () -> void
|
|
52
52
|
def self.build_env_for: (libraries: Array[String], signature_paths: Array[String | _ToPath]) -> untyped
|
|
53
53
|
def self.vendored_gem_sig_paths: () -> Array[Pathname]
|
|
54
|
+
def self.vendored_gem_names: () -> Array[String]
|
|
54
55
|
|
|
55
56
|
def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath], ?cache_store: untyped?) -> void
|
|
56
57
|
def class_known?: (String | Symbol name) -> bool
|
data/sig/rigor/scope.rbs
CHANGED
|
@@ -58,6 +58,7 @@ module Rigor
|
|
|
58
58
|
def with_discovered_def_nodes: (Hash[String, Hash[Symbol, untyped]] table) -> Scope
|
|
59
59
|
def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
60
60
|
def top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
61
|
+
def toplevel?: () -> bool
|
|
61
62
|
def with_discovered_method_visibilities: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
|
|
62
63
|
def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
|
|
63
64
|
def superclass_of: (String | Symbol class_name) -> String?
|