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.
- checksums.yaml +4 -4
- data/README.md +69 -56
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +59 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +11 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +37 -2
- 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
|