Module: Rigor::Environment::BundleSigDiscovery
- Defined in:
- lib/rigor/environment/bundle_sig_discovery.rb
Overview
Open item O4 — target-project Bundler awareness.
Walks a Bundler-installed gem tree (e.g., the project’s ‘vendor/bundle` or a Docker-mounted bundle root) and returns the per-gem `sig/` directories to feed into `RbsLoader`’s ‘signature_paths:`. Of the ~3% of gems that ship `sig/` in their gem package today (per the four-project Mastodon Docker bundle-install measurement on 2026-05-15: 10 of 343 gems shipped sig — `prism`, `aws-sdk-s3`, `aws-sdk-kms`, `aws-sdk-core`, `playwright-ruby-client`, `mutex_m`, `webrick`, `base64`, `stoplight`, `ffi`), this discovery surfaces the typed contract the gem author explicitly published.
Conflicts with rigor’s bundled stdlib RBS (the prism case was the motivating example) degrade gracefully via O7’s failure-memo in ‘RbsLoader#env`: a single warning naming the offending file is emitted and analysis continues with `Dynamic` everywhere rather than hanging.
The discovery is intentionally a pure file-system walk —no ‘Bundler` API call, no `Gemfile.lock` parse — so rigor doesn’t need the target project’s Bundler context.
Constant Summary collapse
- SKIPPED_GEMS_BY_DEFAULT =
Gems already covered by rigor’s ‘DEFAULT_LIBRARIES` (stdlib RBS) plus the `data/vendored_gem_sigs/` bundle. Skipping these from bundle discovery prevents `RBS::DuplicatedDeclarationError` (the prism case was the motivating example — Ruby 4.0 ships prism’s RBS in stdlib, and the gem also ships its own ‘sig/`, so loading both raises on `Prism::BACKEND` etc.).
The list is hard-coded for the MVP because it tracks rigor’s bundled coverage 1:1. When a new gem is vendored under ‘data/vendored_gem_sigs/` or added to `DEFAULT_LIBRARIES`, add its name here.
Set[ # DEFAULT_LIBRARIES (lib/rigor/environment.rb) "pathname", "optparse", "json", "yaml", "fileutils", "tempfile", "tmpdir", "stringio", "forwardable", "digest", "securerandom", "uri", "logger", "date", "pp", "delegate", "singleton", "observable", "abbrev", "find", "tsort", "shellwords", "benchmark", "base64", "did_you_mean", "monitor", "mutex_m", "timeout", "open3", "erb", "etc", "ipaddr", "bigdecimal", "bigdecimal-math", "prettyprint", "random-formatter", "time", "open-uri", "resolv", "csv", "pstore", "objspace", "io-console", "cgi", "cgi-escape", "strscan", "prism", "rbs", # data/vendored_gem_sigs/ "pg", "mysql2", "nokogiri", "bcrypt", "redis", "idn-ruby" ].freeze
Class Method Summary collapse
-
.auto_detect(project_root:) ⇒ Object
Auto-detection order: 1.
-
.discover(bundle_path:, project_root: Dir.pwd, auto_detect: true, skip_gems: SKIPPED_GEMS_BY_DEFAULT, locked_gems: nil) ⇒ Array<Pathname>
Every ‘<gem-dir>/sig` directory under the resolved bundle path, minus any whose gem name is in `skip_gems` and (when `locked_gems` is supplied) minus any whose `(name, version, platform)` does not match a lockfile entry.
-
.gem_name_from_sig_path(sig_dir) ⇒ Object
‘<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig` → `<name>`.
-
.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true) ⇒ Object
Returns ‘Pathname` resolved bundle path, or `nil` when neither explicit nor auto-detected.
Class Method Details
.auto_detect(project_root:) ⇒ Object
Auto-detection order:
-
‘<project_root>/.bundle/config` carries `BUNDLE_PATH:` set by `bundle config set –local path <dir>`.
-
‘<project_root>/vendor/bundle/` — the conventional in-tree install location when a developer ran `bundle install –path vendor/bundle`.
-
‘nil` — let the caller proceed without bundle sig discovery (rigor’s vendored RBS still loads).
168 169 170 171 172 173 174 175 176 |
# File 'lib/rigor/environment/bundle_sig_discovery.rb', line 168 def self.auto_detect(project_root:) from_config = read_bundle_config_path(project_root) return File.(from_config, project_root) if from_config vendor = File.join(project_root, "vendor", "bundle") return vendor if File.directory?(vendor) nil end |
.discover(bundle_path:, project_root: Dir.pwd, auto_detect: true, skip_gems: SKIPPED_GEMS_BY_DEFAULT, locked_gems: nil) ⇒ Array<Pathname>
Returns every ‘<gem-dir>/sig` directory under the resolved bundle path, minus any whose gem name is in `skip_gems` and (when `locked_gems` is supplied) minus any whose `(name, version, platform)` does not match a lockfile entry.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rigor/environment/bundle_sig_discovery.rb', line 86 def self.discover(bundle_path:, project_root: Dir.pwd, auto_detect: true, skip_gems: SKIPPED_GEMS_BY_DEFAULT, locked_gems: nil) resolved = resolve_bundle_path( bundle_path: bundle_path, project_root: project_root, auto_detect: auto_detect ) return [] if resolved.nil? # `<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig/` is the # canonical bundler layout. `*` on the ruby version dir # picks up whichever Ruby the bundle was installed for. all = Dir.glob(resolved.join("ruby", "*", "gems", "*", "sig")).map { |d| Pathname.new(d) } filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) } return filtered if locked_gems.nil? || locked_gems.empty? expected_dirs = expected_gem_dirs(locked_gems) filtered.select { |sig_dir| expected_dirs.include?(sig_dir.parent.basename.to_s) } end |
.gem_name_from_sig_path(sig_dir) ⇒ Object
‘<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig` → `<name>`. The gem directory follows the canonical `<name>-<version>` pattern; we strip everything from the last hyphen onwards to recover the name. (Platform-tagged variants like `ffi-1.17.4-aarch64-linux-gnu/` keep their platform suffix in the version part, so the first hyphen from the right is still the name boundary.)
Public so the O4 Layer 3 slice-3 coverage report (‘RbsCoverageReport`) can classify discovered bundle sigs against locked gem names without re-running discovery.
135 136 137 138 139 140 141 |
# File 'lib/rigor/environment/bundle_sig_discovery.rb', line 135 def self.gem_name_from_sig_path(sig_dir) gem_dir = sig_dir.parent.basename.to_s # Strip `-<version>` and any platform suffix. The version # always starts with a digit, so split at the first # `-` followed by a digit. gem_dir.sub(/-\d.*\z/, "") end |
.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true) ⇒ Object
Returns ‘Pathname` resolved bundle path, or `nil` when neither explicit nor auto-detected. Public for the stats banner so end users can see what rigor picked up.
146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/rigor/environment/bundle_sig_discovery.rb', line 146 def self.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true) if bundle_path path = Pathname.new(File.(bundle_path.to_s, project_root)) return path if path.directory? return nil end return nil unless auto_detect detected = auto_detect(project_root: project_root) Pathname.new(detected) if detected end |