rigortype 0.1.4 → 0.1.6

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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -56
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/fact_store.rb +15 -3
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/result.rb +11 -3
  11. data/lib/rigor/analysis/run_stats.rb +193 -0
  12. data/lib/rigor/analysis/runner.rb +681 -19
  13. data/lib/rigor/analysis/worker_session.rb +339 -0
  14. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  15. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  16. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  17. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  18. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  19. data/lib/rigor/cache/store.rb +72 -9
  20. data/lib/rigor/cli/lsp_command.rb +129 -0
  21. data/lib/rigor/cli/type_of_command.rb +44 -5
  22. data/lib/rigor/cli.rb +122 -10
  23. data/lib/rigor/configuration.rb +168 -7
  24. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  25. data/lib/rigor/environment/class_registry.rb +12 -3
  26. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  27. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  28. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  29. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  30. data/lib/rigor/environment/rbs_loader.rb +238 -7
  31. data/lib/rigor/environment/reflection.rb +152 -0
  32. data/lib/rigor/environment/reporters.rb +40 -0
  33. data/lib/rigor/environment.rb +179 -10
  34. data/lib/rigor/inference/acceptance.rb +83 -4
  35. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  36. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  37. data/lib/rigor/inference/expression_typer.rb +59 -2
  38. data/lib/rigor/inference/hkt_body.rb +171 -0
  39. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  40. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  41. data/lib/rigor/inference/hkt_registry.rb +223 -0
  42. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  44. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  45. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  46. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
  47. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  48. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  49. data/lib/rigor/inference/method_dispatcher.rb +282 -6
  50. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  51. data/lib/rigor/inference/narrowing.rb +127 -8
  52. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  53. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  54. data/lib/rigor/inference/scope_indexer.rb +156 -12
  55. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  56. data/lib/rigor/inference/synthetic_method.rb +86 -0
  57. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
  59. data/lib/rigor/language_server/buffer_table.rb +63 -0
  60. data/lib/rigor/language_server/completion_provider.rb +438 -0
  61. data/lib/rigor/language_server/debouncer.rb +86 -0
  62. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  63. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  64. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  65. data/lib/rigor/language_server/hover_provider.rb +74 -0
  66. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  67. data/lib/rigor/language_server/loop.rb +71 -0
  68. data/lib/rigor/language_server/project_context.rb +145 -0
  69. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  70. data/lib/rigor/language_server/server.rb +384 -0
  71. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  72. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  73. data/lib/rigor/language_server/uri.rb +40 -0
  74. data/lib/rigor/language_server.rb +29 -0
  75. data/lib/rigor/plugin/base.rb +63 -0
  76. data/lib/rigor/plugin/blueprint.rb +60 -0
  77. data/lib/rigor/plugin/loader.rb +3 -1
  78. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  79. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  80. data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
  81. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  82. data/lib/rigor/plugin/macro.rb +31 -0
  83. data/lib/rigor/plugin/manifest.rb +127 -9
  84. data/lib/rigor/plugin/registry.rb +51 -2
  85. data/lib/rigor/plugin.rb +1 -0
  86. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  87. data/lib/rigor/rbs_extended.rb +82 -2
  88. data/lib/rigor/sig_gen/generator.rb +12 -3
  89. data/lib/rigor/trinary.rb +15 -11
  90. data/lib/rigor/type/app.rb +107 -0
  91. data/lib/rigor/type/bot.rb +6 -3
  92. data/lib/rigor/type/combinator.rb +12 -1
  93. data/lib/rigor/type/integer_range.rb +7 -7
  94. data/lib/rigor/type/refined.rb +18 -12
  95. data/lib/rigor/type/top.rb +4 -3
  96. data/lib/rigor/type.rb +1 -0
  97. data/lib/rigor/type_node/generic.rb +7 -1
  98. data/lib/rigor/type_node/identifier.rb +9 -1
  99. data/lib/rigor/type_node/string_literal.rb +4 -1
  100. data/lib/rigor/version.rb +1 -1
  101. data/sig/rigor/environment.rbs +11 -4
  102. data/sig/rigor/inference.rbs +2 -0
  103. data/sig/rigor/plugin/blueprint.rbs +7 -0
  104. data/sig/rigor/plugin/manifest.rbs +1 -1
  105. data/sig/rigor/plugin/registry.rbs +14 -1
  106. data/sig/rigor.rbs +37 -2
  107. metadata +92 -1
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # ADR-20 slice 2e — mutable single-slot memoization
6
+ # container for the per-Environment HKT registry. Held by
7
+ # {Environment} so the otherwise-frozen instance can
8
+ # still cache a computed value on first access.
9
+ #
10
+ # Concurrent {#fetch} calls from multiple threads against
11
+ # one Environment are NOT serialised here — the LSP
12
+ # single-publish-at-a-time discipline and the Ractor
13
+ # pool's per-worker Environment shape already prevent
14
+ # cross-thread races. If a future caller introduces a
15
+ # multi-threaded reader path against a shared
16
+ # Environment, the synchronisation belongs at that
17
+ # caller's seam, not here.
18
+ class HktRegistryHolder
19
+ def initialize
20
+ @loaded = false
21
+ @value = nil
22
+ end
23
+
24
+ def fetch
25
+ return @value if @loaded
26
+
27
+ @value = yield
28
+ @loaded = true
29
+ @value
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Open item O4 Layer 3 — Gemfile.lock parse.
6
+ #
7
+ # Parses a target project's `Gemfile.lock` via Bundler's
8
+ # `LockfileParser` and exposes the locked gem set as a frozen
9
+ # `Hash[String, LockfileResolver::LockedGem]` keyed by gem
10
+ # name. Used by {Rigor::Environment::BundleSigDiscovery} as a
11
+ # filter so the discovered `sig/` directories under the
12
+ # bundler install root are limited to gems the project
13
+ # actually declares (and at the version it declared them).
14
+ #
15
+ # The resolver is intentionally read-only. It does NOT load
16
+ # the project's `Gemfile`, does NOT resolve dependencies,
17
+ # does NOT touch the network, and does NOT require the
18
+ # target project's Bundler context. It only reads bytes from
19
+ # the lockfile.
20
+ #
21
+ # Failure modes are deliberately quiet: a missing or
22
+ # malformed lockfile returns an empty map. The auto-detect
23
+ # path is the configuration default; users who want hard
24
+ # failures should pass an explicit `bundler.lockfile:` and
25
+ # check the result via the stats banner.
26
+ module LockfileResolver
27
+ # Frozen value object for one locked gem entry.
28
+ #
29
+ # `version` is the resolved version string (e.g. "8.0.1");
30
+ # `platform` is the lockfile's platform tag, normalised to
31
+ # `"ruby"` when the lockfile records `ruby` and to the
32
+ # raw String otherwise (e.g. "aarch64-linux-gnu").
33
+ LockedGem = Data.define(:name, :version, :platform) do
34
+ def initialize(name:, version:, platform:)
35
+ super(
36
+ name: -name.to_s,
37
+ version: -version.to_s,
38
+ platform: -platform.to_s
39
+ )
40
+ end
41
+ end
42
+
43
+ # @param lockfile_path [String, Pathname, nil] explicit path
44
+ # to the Gemfile.lock. When `nil`, falls back to
45
+ # `auto_detect` if `auto_detect:` is true.
46
+ # @param project_root [String] resolution base for a
47
+ # relative `lockfile_path:` and the auto-detect search.
48
+ # @param auto_detect [Boolean] when true and
49
+ # `lockfile_path:` is nil, look for
50
+ # `<project_root>/Gemfile.lock`.
51
+ # @return [Hash{String => LockedGem}] frozen map of gem
52
+ # name → locked entry. Returns the empty frozen hash
53
+ # when no lockfile is resolvable, when the file is
54
+ # unreadable, or when Bundler refuses to parse it.
55
+ def self.locked_gems(lockfile_path:, project_root: Dir.pwd, auto_detect: true)
56
+ resolved = resolve_lockfile_path(
57
+ lockfile_path: lockfile_path,
58
+ project_root: project_root,
59
+ auto_detect: auto_detect
60
+ )
61
+ return EMPTY unless resolved
62
+
63
+ parse(resolved)
64
+ end
65
+
66
+ # Returns the resolved lockfile path (`Pathname`) or `nil`
67
+ # when neither explicit nor auto-detect produces one.
68
+ # Public so the stats banner can show what rigor picked up.
69
+ def self.resolve_lockfile_path(lockfile_path:, project_root: Dir.pwd, auto_detect: true)
70
+ if lockfile_path
71
+ path = Pathname.new(File.expand_path(lockfile_path.to_s, project_root))
72
+ return path if path.file?
73
+
74
+ return nil
75
+ end
76
+
77
+ return nil unless auto_detect
78
+
79
+ candidate = Pathname.new(File.join(project_root, "Gemfile.lock"))
80
+ candidate.file? ? candidate : nil
81
+ end
82
+
83
+ EMPTY = {}.freeze
84
+ private_constant :EMPTY
85
+
86
+ # Parses a Gemfile.lock at the given path. Bundler load
87
+ # errors and malformed lockfile bytes both surface as the
88
+ # empty frozen hash; analysis must not crash because a
89
+ # lockfile is malformed. A single warning is emitted to
90
+ # `$stderr` so the user can see why their lockfile was
91
+ # ignored.
92
+ def self.parse(path)
93
+ require "bundler"
94
+ rescue LoadError => e
95
+ warn "rigor: cannot read #{path}: bundler is not available (#{e.message})"
96
+ EMPTY
97
+ else
98
+ do_parse(path)
99
+ end
100
+ private_class_method :parse
101
+
102
+ def self.do_parse(path)
103
+ body = File.read(path.to_s)
104
+ parser = Bundler::LockfileParser.new(body)
105
+ locked = parser.specs.each_with_object({}) do |spec, h|
106
+ # `Bundler::LazySpecification` carries name, version,
107
+ # platform. Platform is `Gem::Platform` or the symbol
108
+ # `:ruby`; both stringify cleanly. The upstream
109
+ # bundler RBS shim (references/rbs/sig/shims/bundler.rbs)
110
+ # does NOT declare `LazySpecification#platform` so the
111
+ # call site needs a suppression marker.
112
+ platform = spec.platform.to_s # rigor:disable undefined-method
113
+ h[spec.name.to_s] = LockedGem.new(
114
+ name: spec.name, version: spec.version.to_s, platform: platform
115
+ )
116
+ end
117
+ locked.freeze
118
+ rescue StandardError => e
119
+ warn "rigor: ignoring malformed #{path} (#{e.class}: #{e.message})"
120
+ EMPTY
121
+ end
122
+ private_class_method :do_parse
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Rigor
6
+ class Environment
7
+ # Open item O4 Layer 3 slice 2 — `rbs collection install`
8
+ # awareness.
9
+ #
10
+ # When the target project has been set up with `rbs
11
+ # collection install` (the standard RBS-ecosystem flow for
12
+ # pulling community RBS from
13
+ # https://github.com/ruby/gem_rbs_collection), a
14
+ # `rbs_collection.lock.yaml` records the resolved (gem,
15
+ # version, source) triples and `.gem_rbs_collection/<name>/
16
+ # <version>/` carries the actual `.rbs` files. This module
17
+ # parses the lockfile and returns the per-gem RBS directory
18
+ # paths so they can be appended to `RbsLoader`'s
19
+ # `signature_paths:`.
20
+ #
21
+ # The discovery is intentionally a pure file-system + YAML
22
+ # walk — no Bundler API call, no network access. Failure
23
+ # modes (missing lockfile, malformed YAML, missing
24
+ # collection directory) silently degrade to an empty list.
25
+ module RbsCollectionDiscovery
26
+ # `stdlib`-typed entries in the lockfile are loaded into
27
+ # the RBS environment by the standard library mechanism
28
+ # (rigor's `Environment::DEFAULT_LIBRARIES` already covers
29
+ # this surface). Including them as `signature_paths:`
30
+ # entries would risk `RBS::DuplicatedDeclarationError`
31
+ # (the same hazard O7's failure-memo handles). The other
32
+ # documented source types — `git` (the gem_rbs_collection
33
+ # repo), `rubygems` (sigs lifted from a gem's bundled
34
+ # `sig/`), and `local` (a user-managed RBS dir) — all
35
+ # produce a directory under the collection root and are
36
+ # admitted.
37
+ SKIPPED_SOURCE_TYPES = Set["stdlib"].freeze
38
+
39
+ DEFAULT_COLLECTION_PATH = ".gem_rbs_collection"
40
+ private_constant :DEFAULT_COLLECTION_PATH
41
+
42
+ # @param lockfile_path [String, Pathname, nil] explicit
43
+ # path to `rbs_collection.lock.yaml`. When `nil`, falls
44
+ # back to `auto_detect` if `auto_detect:` is true.
45
+ # @param project_root [String] resolution base for
46
+ # relative `lockfile_path:` and the auto-detect search.
47
+ # @param auto_detect [Boolean] when true and
48
+ # `lockfile_path:` is nil, look for
49
+ # `<project_root>/rbs_collection.lock.yaml`.
50
+ # @return [Array<Pathname>] every
51
+ # `<collection_path>/<gem-name>/<gem-version>/`
52
+ # directory listed in the lockfile whose entry has a
53
+ # non-skipped source type and whose directory exists on
54
+ # disk. Returns `[]` when no lockfile is resolvable,
55
+ # when the YAML is unreadable, or when the collection
56
+ # path doesn't exist.
57
+ def self.discover(lockfile_path:, project_root: Dir.pwd, auto_detect: true)
58
+ resolved = resolve_lockfile_path(
59
+ lockfile_path: lockfile_path,
60
+ project_root: project_root,
61
+ auto_detect: auto_detect
62
+ )
63
+ return [] if resolved.nil?
64
+
65
+ data = read_lockfile_yaml(resolved)
66
+ return [] if data.nil?
67
+
68
+ collection_root = resolve_collection_root(resolved, data)
69
+ return [] unless collection_root.directory?
70
+
71
+ gem_paths_from(collection_root, data)
72
+ end
73
+
74
+ # Returns the resolved lockfile path (`Pathname`) or `nil`
75
+ # when neither explicit nor auto-detect produces one.
76
+ # Public so the stats banner can surface what rigor found.
77
+ def self.resolve_lockfile_path(lockfile_path:, project_root: Dir.pwd, auto_detect: true)
78
+ if lockfile_path
79
+ path = Pathname.new(File.expand_path(lockfile_path.to_s, project_root))
80
+ return path if path.file?
81
+
82
+ return nil
83
+ end
84
+
85
+ return nil unless auto_detect
86
+
87
+ candidate = Pathname.new(File.join(project_root, "rbs_collection.lock.yaml"))
88
+ candidate.file? ? candidate : nil
89
+ end
90
+
91
+ def self.read_lockfile_yaml(path)
92
+ data = YAML.safe_load_file(path.to_s, aliases: false)
93
+ data.is_a?(Hash) ? data : nil
94
+ rescue StandardError
95
+ nil
96
+ end
97
+ private_class_method :read_lockfile_yaml
98
+
99
+ def self.resolve_collection_root(lockfile_pathname, data)
100
+ rel = data["path"]
101
+ rel = DEFAULT_COLLECTION_PATH if rel.nil? || rel.to_s.empty?
102
+ # `path:` is documented as relative to the directory
103
+ # holding the lockfile (RBS::Collection::Config::Lockfile#fullpath).
104
+ lockfile_pathname.parent + Pathname.new(rel.to_s)
105
+ end
106
+ private_class_method :resolve_collection_root
107
+
108
+ def self.gem_paths_from(collection_root, data)
109
+ Array(data["gems"]).filter_map do |entry|
110
+ next unless entry.is_a?(Hash)
111
+
112
+ source_type = entry.dig("source", "type").to_s
113
+ next if SKIPPED_SOURCE_TYPES.include?(source_type)
114
+
115
+ name = entry["name"]
116
+ version = entry["version"]
117
+ next if name.nil? || version.nil?
118
+
119
+ gem_root = collection_root + name.to_s + version.to_s
120
+ gem_root if gem_root.directory?
121
+ end
122
+ end
123
+ private_class_method :gem_paths_from
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Open item O4 Layer 3 slice 3 — graceful-degradation
6
+ # coverage report.
7
+ #
8
+ # When the user has a `Gemfile.lock` (via slice 1) and rigor
9
+ # has resolved its target-project RBS sources (DEFAULT_LIBRARIES,
10
+ # `data/vendored_gem_sigs/`, slice-1 bundle-shipped `sig/`,
11
+ # slice-2 `rbs_collection.lock.yaml` paths), this module
12
+ # classifies each locked gem by RBS provenance and surfaces
13
+ # the "no RBS available" set so the run-start diagnostic in
14
+ # {Rigor::Analysis::Runner} can suggest `rbs collection
15
+ # install` or `dependencies.source_inference:` for the
16
+ # uncovered gems.
17
+ #
18
+ # The classification is a pure function over the inputs
19
+ # (`locked_gems`, two arrays of resolved sig paths). It does
20
+ # NOT touch the filesystem on its own — the caller passes in
21
+ # what discovery returned.
22
+ module RbsCoverageReport
23
+ # Frozen result row.
24
+ #
25
+ # `source` is a Symbol naming where RBS for this gem
26
+ # resolves; `:missing` means none of the four resolution
27
+ # paths covered it.
28
+ Coverage = Data.define(:gem_name, :version, :source) do
29
+ def initialize(gem_name:, version:, source:)
30
+ super(
31
+ gem_name: -gem_name.to_s,
32
+ version: -version.to_s,
33
+ source: source
34
+ )
35
+ end
36
+ end
37
+
38
+ # Names of gems whose RBS ships under
39
+ # `data/vendored_gem_sigs/`. Kept in sync with the
40
+ # vendored-stubs directory listing; when a new gem is
41
+ # vendored, add its name here too. (The set is small
42
+ # enough that hard-coding is acceptable; a directory walk
43
+ # at every call would add stat-cost to no benefit.)
44
+ VENDORED_GEM_NAMES = Set[
45
+ "bcrypt", "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis"
46
+ ].freeze
47
+
48
+ # @param locked_gems [Hash{String => LockfileResolver::LockedGem}]
49
+ # The lockfile-resolved gem set. Empty hash → no
50
+ # coverage analysis to do.
51
+ # @param default_libraries [Array<String>] gem names rigor
52
+ # auto-loads through `RBS::EnvironmentLoader#add(library:)`.
53
+ # Pass `Rigor::Environment::DEFAULT_LIBRARIES` from callers
54
+ # running in a project context.
55
+ # @param bundle_sig_paths [Array<Pathname, String>] the
56
+ # discovered `<bundle>/.../gems/<name>-<ver>/sig` paths
57
+ # from {BundleSigDiscovery.discover}.
58
+ # @param rbs_collection_paths [Array<Pathname, String>] the
59
+ # discovered `<collection>/<name>/<version>/` paths from
60
+ # {RbsCollectionDiscovery.discover}.
61
+ # @return [Array<Coverage>] one row per locked gem; sorted
62
+ # by gem name for deterministic output.
63
+ def self.classify(locked_gems:, default_libraries:,
64
+ bundle_sig_paths:, rbs_collection_paths:)
65
+ default_set = default_libraries.to_set
66
+ bundle_names = extract_gem_names_from_bundle_paths(bundle_sig_paths)
67
+ collection_names = extract_gem_names_from_collection_paths(rbs_collection_paths)
68
+
69
+ locked_gems.each_value.map do |locked|
70
+ name = locked.name
71
+ source = if default_set.include?(name)
72
+ :default_library
73
+ elsif VENDORED_GEM_NAMES.include?(name)
74
+ :vendored_gem_sig
75
+ elsif bundle_names.include?(name)
76
+ :bundle_sig
77
+ elsif collection_names.include?(name)
78
+ :rbs_collection
79
+ else
80
+ :missing
81
+ end
82
+ Coverage.new(gem_name: name, version: locked.version, source: source)
83
+ end.sort_by(&:gem_name)
84
+ end
85
+
86
+ # Convenience accessor for the run-start diagnostic.
87
+ # Filters {classify} down to `:missing` rows.
88
+ def self.missing(coverage_rows)
89
+ coverage_rows.select { |row| row.source == :missing }
90
+ end
91
+
92
+ def self.extract_gem_names_from_bundle_paths(paths)
93
+ paths.each_with_object(Set.new) do |path, set|
94
+ pathname = path.is_a?(Pathname) ? path : Pathname.new(path)
95
+ set << BundleSigDiscovery.gem_name_from_sig_path(pathname)
96
+ end
97
+ end
98
+ private_class_method :extract_gem_names_from_bundle_paths
99
+
100
+ def self.extract_gem_names_from_collection_paths(paths)
101
+ # `RbsCollectionDiscovery.discover` returns
102
+ # `<collection_root>/<name>/<version>/` so the parent
103
+ # basename is the gem name.
104
+ paths.each_with_object(Set.new) do |path, set|
105
+ pathname = path.is_a?(Pathname) ? path : Pathname.new(path)
106
+ set << pathname.parent.basename.to_s
107
+ end
108
+ end
109
+ private_class_method :extract_gem_names_from_collection_paths
110
+ end
111
+ end
112
+ end