rigortype 0.2.0 → 0.2.2
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 +82 -20
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +120 -1
data/lib/rigor/cli.rb
CHANGED
|
@@ -16,8 +16,10 @@ require_relative "cli/ci_detector"
|
|
|
16
16
|
module Rigor
|
|
17
17
|
# The CLI class is a dispatcher: each `run_*` method delegates to a
|
|
18
18
|
# command-specific class once the command grows beyond a few lines (see
|
|
19
|
-
# {CLI::TypeOfCommand} and {CLI::CheckCommand}).
|
|
20
|
-
|
|
19
|
+
# {CLI::TypeOfCommand} and {CLI::CheckCommand}). It necessarily grows by
|
|
20
|
+
# one delegator + one help line per command, so — like the command
|
|
21
|
+
# classes it fans out to — it carries an explicit ClassLength exemption.
|
|
22
|
+
class CLI # rubocop:disable Metrics/ClassLength
|
|
21
23
|
EXIT_USAGE = 64
|
|
22
24
|
|
|
23
25
|
HANDLERS = {
|
|
@@ -39,6 +41,8 @@ module Rigor
|
|
|
39
41
|
"plugin" => :run_plugin,
|
|
40
42
|
"playground" => :run_playground,
|
|
41
43
|
"skill" => :run_skill,
|
|
44
|
+
"describe" => :run_describe,
|
|
45
|
+
"docs" => :run_docs,
|
|
42
46
|
"show-bleedingedge" => :run_show_bleedingedge
|
|
43
47
|
}.freeze
|
|
44
48
|
|
|
@@ -285,6 +289,22 @@ module Rigor
|
|
|
285
289
|
CLI::SkillCommand.new(argv: @argv, out: @out, err: @err).run
|
|
286
290
|
end
|
|
287
291
|
|
|
292
|
+
# `rigor describe` — a top-level alias for `rigor skill describe`,
|
|
293
|
+
# the entry point most users reach for first. Surfaced because a
|
|
294
|
+
# bare `rigor describe` is the intuitive guess (the onboarding field
|
|
295
|
+
# trial saw it tried and met "Unknown command").
|
|
296
|
+
def run_describe
|
|
297
|
+
require_relative "cli/skill_command"
|
|
298
|
+
|
|
299
|
+
CLI::SkillCommand.new(argv: ["describe", *@argv], out: @out, err: @err).run
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def run_docs
|
|
303
|
+
require_relative "cli/docs_command"
|
|
304
|
+
|
|
305
|
+
CLI::DocsCommand.new(argv: @argv, out: @out, err: @err).run
|
|
306
|
+
end
|
|
307
|
+
|
|
288
308
|
def run_plugin
|
|
289
309
|
require_relative "cli/plugin_command"
|
|
290
310
|
|
|
@@ -318,7 +338,9 @@ module Rigor
|
|
|
318
338
|
plugins Report activation status of every configured plugin
|
|
319
339
|
plugin Browse bundled plugin source as worked examples (list/path/print/root)
|
|
320
340
|
playground Start the browser playground (requires rigor-playground gem)
|
|
321
|
-
|
|
341
|
+
describe Recommend the next skill for this project (alias for `skill describe`)
|
|
342
|
+
skill Recommend the next skill + list/print bundled Agent Skills (skill describe, skill <name>)
|
|
343
|
+
docs Print the bundled docs offline (docs <name>, docs --list)
|
|
322
344
|
show-bleedingedge Show the bleeding-edge overlay + what your config adopts (ADR-50)
|
|
323
345
|
version Print the Rigor version
|
|
324
346
|
help Print this help
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "signature_path_audit"
|
|
4
|
+
require_relative "analysis/check_rules"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
# Audits a loaded {Configuration} for the class of mistake where a
|
|
8
|
+
# configured value silently resolves to nothing — a typo'd or moved
|
|
9
|
+
# path, an unknown library name, an inert rule id. The shared failure
|
|
10
|
+
# mode is that the loader filters the bad entry without a word, so the
|
|
11
|
+
# only symptom is downstream and confusing: missing signatures turn
|
|
12
|
+
# every call into the types they were meant to cover into a
|
|
13
|
+
# high-confidence `call.undefined-method`, and an unrecognised
|
|
14
|
+
# suppression token leaves the rule firing as if the `disable:` line
|
|
15
|
+
# were never written. This module surfaces each such entry up front so
|
|
16
|
+
# the cause is visible instead of inferred.
|
|
17
|
+
#
|
|
18
|
+
# Every check is held to the same bar that {SignaturePathAudit} set:
|
|
19
|
+
# it mirrors the loader's own acceptance test, so a warning means the
|
|
20
|
+
# loader really did load nothing, and it never fires on a setup that
|
|
21
|
+
# works. In particular the rule-token check only flags a token under a
|
|
22
|
+
# built-in family (`call`, `flow`, …) — a plugin- or `rbs_extended.*`
|
|
23
|
+
# rule id (whose family Rigor cannot statically enumerate, since a
|
|
24
|
+
# `node_rule` block emits any `rule:` string it likes) is left alone, so
|
|
25
|
+
# disabling a plugin rule is never mistaken for a typo.
|
|
26
|
+
module ConfigAudit
|
|
27
|
+
# One config-level finding. `kind` discriminates the source key
|
|
28
|
+
# (`:signature_path`, `:library`, `:disabled_rule`,
|
|
29
|
+
# `:severity_override`, `:bundler_bundle_path`, `:bundler_lockfile`,
|
|
30
|
+
# `:rbs_collection_lockfile`); `fields` carries the kind-specific
|
|
31
|
+
# structured data merged into {#to_h} for JSON consumers.
|
|
32
|
+
Warning = Data.define(:kind, :message, :fields) do
|
|
33
|
+
def to_h
|
|
34
|
+
{ "kind" => kind.to_s, "message" => message }.merge(fields)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param configuration [Rigor::Configuration]
|
|
39
|
+
# @param project_root [String] the directory the run's relative
|
|
40
|
+
# bundler / collection paths resolve against (the CLI's CWD), used
|
|
41
|
+
# only by the explicit-path checks.
|
|
42
|
+
# @return [Array<Warning>]
|
|
43
|
+
def self.warnings(configuration, project_root: Dir.pwd)
|
|
44
|
+
signature_path_warnings(configuration) +
|
|
45
|
+
library_warnings(configuration) +
|
|
46
|
+
rule_token_warnings(configuration) +
|
|
47
|
+
explicit_path_warnings(configuration, project_root)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `signature_paths:` entries that resolve to nothing — delegated to
|
|
51
|
+
# {SignaturePathAudit}, which mirrors the loader's `directory?` +
|
|
52
|
+
# recursive `**/*.rbs` acceptance test.
|
|
53
|
+
def self.signature_path_warnings(configuration)
|
|
54
|
+
SignaturePathAudit.warnings(configuration.signature_paths).map do |entry|
|
|
55
|
+
Warning.new(
|
|
56
|
+
kind: :signature_path,
|
|
57
|
+
message: entry.message,
|
|
58
|
+
fields: { "path" => entry.path, "status" => entry.status.to_s, "rbs_file_count" => entry.rbs_file_count }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# `libraries:` entries RBS does not recognise. Uses the same
|
|
64
|
+
# `RBS::EnvironmentLoader#has_library?` guard the loader filters
|
|
65
|
+
# through ({Environment::RbsLoader} `build_env_for`), so a flagged
|
|
66
|
+
# name is exactly one the loader skipped. Fails soft: if RBS itself
|
|
67
|
+
# raises, no warning rather than a crash.
|
|
68
|
+
def self.library_warnings(configuration)
|
|
69
|
+
configured = Array(configuration.libraries)
|
|
70
|
+
return [] if configured.empty?
|
|
71
|
+
|
|
72
|
+
loader = ::RBS::EnvironmentLoader.new
|
|
73
|
+
configured.reject { |lib| loader.has_library?(library: lib.to_s, version: nil) }.map do |lib|
|
|
74
|
+
Warning.new(
|
|
75
|
+
kind: :library,
|
|
76
|
+
message: "libraries: #{lib.to_s.inspect} is not an available RBS library (no signatures loaded from it)",
|
|
77
|
+
fields: { "name" => lib.to_s }
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
rescue StandardError
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `disable:` tokens and `severity_overrides:` keys that name no rule.
|
|
85
|
+
# Restricted to tokens under a built-in family so a plugin rule id is
|
|
86
|
+
# never mis-flagged (see the module docstring).
|
|
87
|
+
def self.rule_token_warnings(configuration)
|
|
88
|
+
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
89
|
+
next unless inert_builtin_token?(token.to_s)
|
|
90
|
+
|
|
91
|
+
Warning.new(
|
|
92
|
+
kind: :disabled_rule,
|
|
93
|
+
message: "disable: #{token.to_s.inspect} is not a recognized rule id; the suppression has no effect",
|
|
94
|
+
fields: { "token" => token.to_s }
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
overrides = configuration.severity_overrides.keys.filter_map do |key|
|
|
98
|
+
next unless inert_builtin_token?(key.to_s)
|
|
99
|
+
|
|
100
|
+
Warning.new(
|
|
101
|
+
kind: :severity_override,
|
|
102
|
+
message: "severity_overrides: #{key.to_s.inspect} is not a recognized rule id; the override has no effect",
|
|
103
|
+
fields: { "token" => key.to_s }
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
disable + overrides
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# True when `token` looks like a built-in rule id but matches none —
|
|
110
|
+
# its first segment is a built-in family yet it is neither a bare
|
|
111
|
+
# family wildcard nor a known canonical id. A token whose family is
|
|
112
|
+
# not built-in (a plugin / `rbs_extended.*` rule, or a bare legacy
|
|
113
|
+
# alias) is deliberately NOT flagged: it may resolve at run time, so
|
|
114
|
+
# under-warning is the FP-safe choice.
|
|
115
|
+
def self.inert_builtin_token?(token)
|
|
116
|
+
family = token.split(".").first
|
|
117
|
+
return false unless Analysis::CheckRules::RULE_FAMILIES.include?(family)
|
|
118
|
+
return false if token == family
|
|
119
|
+
return false if Analysis::CheckRules::ALL_RULES.include?(token)
|
|
120
|
+
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Explicitly-configured bundler / rbs-collection paths that do not
|
|
125
|
+
# exist. Only the explicit form is audited (a nil value means
|
|
126
|
+
# auto-detection, which finding nothing is normal); messages stay
|
|
127
|
+
# factual about the path rather than guessing the fallback.
|
|
128
|
+
def self.explicit_path_warnings(configuration, project_root)
|
|
129
|
+
warnings = []
|
|
130
|
+
add_missing_dir(warnings, configuration.bundler_bundle_path, project_root,
|
|
131
|
+
:bundler_bundle_path, "bundler.bundle_path")
|
|
132
|
+
add_missing_file(warnings, configuration.bundler_lockfile, project_root,
|
|
133
|
+
:bundler_lockfile, "bundler.lockfile")
|
|
134
|
+
add_missing_file(warnings, configuration.rbs_collection_lockfile, project_root,
|
|
135
|
+
:rbs_collection_lockfile, "rbs_collection.lockfile")
|
|
136
|
+
warnings
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.add_missing_dir(warnings, path, project_root, kind, key)
|
|
140
|
+
return if path.nil? || File.directory?(File.expand_path(path, project_root))
|
|
141
|
+
|
|
142
|
+
warnings << Warning.new(kind: kind, message: "#{key}: #{path.inspect} is not a directory",
|
|
143
|
+
fields: { "path" => path })
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.add_missing_file(warnings, path, project_root, kind, key)
|
|
147
|
+
return if path.nil? || File.file?(File.expand_path(path, project_root))
|
|
148
|
+
|
|
149
|
+
warnings << Warning.new(kind: kind, message: "#{key}: #{path.inspect} does not exist", fields: { "path" => path })
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -600,6 +600,18 @@ module Rigor
|
|
|
600
600
|
raise ArgumentError, "severity_overrides must be a Hash, got #{value.inspect}" unless value.is_a?(Hash)
|
|
601
601
|
|
|
602
602
|
value.to_h do |k, v|
|
|
603
|
+
# YAML 1.1 parses bare `off`/`on`/`no`/`yes`/`true`/`false`
|
|
604
|
+
# as booleans, so a user who wrote `off` (a valid severity)
|
|
605
|
+
# without quotes hands us `false`. Catch the non-Symbol /
|
|
606
|
+
# non-String case before `to_sym` blows up with a backtrace.
|
|
607
|
+
unless v.is_a?(String) || v.is_a?(Symbol)
|
|
608
|
+
hint = v == false ? %( — did you mean the string "off"?) : ""
|
|
609
|
+
raise ArgumentError,
|
|
610
|
+
"severity_overrides[#{k.inspect}] is #{v.inspect}, a YAML boolean#{hint} " \
|
|
611
|
+
"Bare off/on/no/yes/true/false are parsed as booleans; quote the severity " \
|
|
612
|
+
"(e.g. \"off\")."
|
|
613
|
+
end
|
|
614
|
+
|
|
603
615
|
sym = v.to_sym
|
|
604
616
|
unless SeverityProfile::VALID_SEVERITIES.include?(sym)
|
|
605
617
|
raise ArgumentError,
|
|
@@ -323,6 +323,33 @@ module Rigor
|
|
|
323
323
|
[Pathname(CORE_OVERLAY_SIGS_ROOT)]
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
+
# Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`),
|
|
327
|
+
# ADR-72. Unlike the unconditional `core_overlay`, each gem's
|
|
328
|
+
# overlay is loaded ONLY when that gem is locked in the
|
|
329
|
+
# project's Gemfile.lock but ships no RBS of its own —
|
|
330
|
+
# {Environment.for_project} decides eligibility and passes the
|
|
331
|
+
# already-filtered gem-name set here. One directory per gem name
|
|
332
|
+
# keeps the membership check a cheap `File.directory?`.
|
|
333
|
+
GEM_OVERLAY_SIGS_ROOT = File.expand_path(
|
|
334
|
+
"../../../data/gem_overlay",
|
|
335
|
+
__dir__
|
|
336
|
+
).freeze
|
|
337
|
+
|
|
338
|
+
# @param gem_names [Enumerable<String>] overlay-eligible
|
|
339
|
+
# Gemfile.lock gem names (the caller filters to the
|
|
340
|
+
# `:missing`-coverage, no-conflicting-plugin set).
|
|
341
|
+
# @return [Array<Pathname>] the bundled overlay directory for
|
|
342
|
+
# each gem that ships one; empty when none match or the
|
|
343
|
+
# overlay root is absent.
|
|
344
|
+
def gem_overlay_sig_paths(gem_names)
|
|
345
|
+
return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT)
|
|
346
|
+
|
|
347
|
+
gem_names.filter_map do |name|
|
|
348
|
+
dir = File.join(GEM_OVERLAY_SIGS_ROOT, name.to_s)
|
|
349
|
+
Pathname(dir) if File.directory?(dir)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
326
353
|
def vendored_gem_sig_paths
|
|
327
354
|
return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
|
|
328
355
|
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -65,6 +65,13 @@ module Rigor
|
|
|
65
65
|
prism rbs
|
|
66
66
|
].freeze
|
|
67
67
|
|
|
68
|
+
# ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id
|
|
69
|
+
# that ships the SAME core-ext RBS. When that plugin is loaded the
|
|
70
|
+
# auto-overlay for the gem stands down, so the two never both
|
|
71
|
+
# declare the methods (which would raise a duplicate-declaration
|
|
72
|
+
# error). Keyed on the gem name `RbsCoverageReport` reports.
|
|
73
|
+
GEM_OVERLAY_PLUGIN_IDS = { "activesupport" => "activesupport-core-ext" }.freeze
|
|
74
|
+
|
|
68
75
|
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
69
76
|
:reporters, :name_scope,
|
|
70
77
|
:synthetic_method_index, :project_patched_methods
|
|
@@ -291,7 +298,24 @@ module Rigor
|
|
|
291
298
|
# collection discovery. A duplicate-declaration conflict
|
|
292
299
|
# degrades through the same O7 failure-memo path.
|
|
293
300
|
plugin_sig_paths = plugin_registry ? plugin_registry.signature_paths.map(&:to_s) : []
|
|
294
|
-
|
|
301
|
+
# ADR-72 — Gemfile.lock-gated bundled RBS overlays. For each
|
|
302
|
+
# locked gem that ships no RBS through any resolution path
|
|
303
|
+
# (`:missing`) and has a Rigor-bundled overlay, load that
|
|
304
|
+
# overlay so its core-class extensions resolve (e.g.
|
|
305
|
+
# `Integer#minutes` on a Rails project) — turning a systematic
|
|
306
|
+
# false `call.undefined-method` into a no-op while a project
|
|
307
|
+
# WITHOUT the gem still sees the genuine diagnostic. Appended
|
|
308
|
+
# last so any RBS the project already supplies wins, and skipped
|
|
309
|
+
# for a gem whose opt-in plugin twin is loaded (no duplicate
|
|
310
|
+
# declaration). The paths ride in `loader_signature_paths`, so
|
|
311
|
+
# the env cache descriptor digests them for free.
|
|
312
|
+
overlay_paths = gem_overlay_paths(
|
|
313
|
+
locked: locked, default_libraries: merged_libraries,
|
|
314
|
+
bundle_sig_paths: gem_sig_paths, rbs_collection_paths: collection_paths,
|
|
315
|
+
plugin_registry: plugin_registry
|
|
316
|
+
)
|
|
317
|
+
loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths +
|
|
318
|
+
collection_paths + overlay_paths
|
|
295
319
|
# ADR-32 WD4 + WD5 — invoke each loaded plugin's
|
|
296
320
|
# `source_rbs_synthesizer` once per project source file
|
|
297
321
|
# and collect non-nil `[filename, rbs_source]` pairs.
|
|
@@ -346,6 +370,30 @@ module Rigor
|
|
|
346
370
|
sig.directory? ? [sig] : []
|
|
347
371
|
end
|
|
348
372
|
|
|
373
|
+
# ADR-72 — resolve the bundled RBS overlay directories to load for
|
|
374
|
+
# this project. A gem is eligible when it is locked in the
|
|
375
|
+
# Gemfile.lock, classified `:missing` by {RbsCoverageReport}
|
|
376
|
+
# (no RBS through default-library / vendored / bundle-`sig/` /
|
|
377
|
+
# rbs-collection), and its conflicting opt-in plugin (if any) is
|
|
378
|
+
# not loaded. Returns `[Pathname]`, deterministically ordered, or
|
|
379
|
+
# `[]` when no lockfile / no eligible gem.
|
|
380
|
+
def gem_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
|
|
381
|
+
rbs_collection_paths:, plugin_registry:)
|
|
382
|
+
return [] if locked.empty?
|
|
383
|
+
|
|
384
|
+
missing = RbsCoverageReport.classify(
|
|
385
|
+
locked_gems: locked, default_libraries: default_libraries,
|
|
386
|
+
bundle_sig_paths: bundle_sig_paths, rbs_collection_paths: rbs_collection_paths
|
|
387
|
+
).select { |row| row.source == :missing }.map(&:gem_name)
|
|
388
|
+
|
|
389
|
+
loaded_ids = plugin_registry ? plugin_registry.ids.to_set : Set.new
|
|
390
|
+
eligible = missing.reject do |gem_name|
|
|
391
|
+
plugin_id = GEM_OVERLAY_PLUGIN_IDS[gem_name]
|
|
392
|
+
plugin_id && loaded_ids.include?(plugin_id)
|
|
393
|
+
end.sort
|
|
394
|
+
RbsLoader.gem_overlay_sig_paths(eligible)
|
|
395
|
+
end
|
|
396
|
+
|
|
349
397
|
# ADR-32 WD4 + WD5 — for each project source file, invoke
|
|
350
398
|
# every plugin-registered synthesizer once and collect
|
|
351
399
|
# non-nil returns. The returned array is `[[virtual_filename,
|
|
@@ -51,7 +51,15 @@ module Rigor
|
|
|
51
51
|
NUMERIC_BINARY = Set[
|
|
52
52
|
:+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
|
|
53
53
|
:<, :<=, :>, :>=, :==, :!=, :<=>,
|
|
54
|
-
:gcd, :lcm, :fdiv, :quo, :ceildiv, :[]
|
|
54
|
+
:gcd, :lcm, :fdiv, :quo, :ceildiv, :[],
|
|
55
|
+
# Integer bit-test predicates (`(self & mask) <=> mask|0`). The
|
|
56
|
+
# catalog marks them `:dispatch` only because a non-Integer mask
|
|
57
|
+
# would route through `to_int`; a concrete Integer literal never
|
|
58
|
+
# does, so the fold is pure here — the sibling of the already-folded
|
|
59
|
+
# bit-reference `:[]`. Integer-only, but Float-safe to list: a Float
|
|
60
|
+
# receiver has no such method, so `invoke_binary` rescues the
|
|
61
|
+
# `NoMethodError` to nil and the RBS tier answers.
|
|
62
|
+
:allbits?, :anybits?, :nobits?
|
|
55
63
|
].freeze
|
|
56
64
|
STRING_BINARY = Set[
|
|
57
65
|
:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
|
|
@@ -86,6 +94,17 @@ module Rigor
|
|
|
86
94
|
# delegates to operand `==`); ordering is undefined for Complex.
|
|
87
95
|
COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
|
|
88
96
|
|
|
97
|
+
# `Set#&` and its alias `Set#intersection` are leaf-pure for a
|
|
98
|
+
# concrete Set operand exactly like their siblings `|` / `-` / `^`
|
|
99
|
+
# (all `:leaf` in the catalog), but the catalog flags
|
|
100
|
+
# `set_i_intersection`'s C body `block_dependent` — it drives Set's
|
|
101
|
+
# own internal iterator — so the catalog tier declines and the
|
|
102
|
+
# intersection alone fails to fold. A concrete Set argument's `each`
|
|
103
|
+
# is the pure core method, so the fold is sound; the hand-rolled
|
|
104
|
+
# allow-list is the right tool, mirroring the bit-test predicates.
|
|
105
|
+
# (The other binary set ops keep folding through the catalog.)
|
|
106
|
+
SET_BINARY = Set[:&, :intersection].freeze
|
|
107
|
+
|
|
89
108
|
# v0.0.3 C — pure unary catalogue. Each method must:
|
|
90
109
|
# - take zero arguments,
|
|
91
110
|
# - have no side effects,
|
|
@@ -114,17 +133,35 @@ module Rigor
|
|
|
114
133
|
# siblings `:inspect` / `:to_s` remain folded.
|
|
115
134
|
INTEGER_UNARY = Set[
|
|
116
135
|
:odd?, :even?, :zero?, :positive?, :negative?,
|
|
136
|
+
# `finite?` / `infinite?` are total on Integer (`true` / `nil`
|
|
137
|
+
# always) and round out the numeric predicate family — the Float
|
|
138
|
+
# sibling already folds them. `nonzero?` returns `self` (non-zero)
|
|
139
|
+
# or `nil`, both foldable Constants.
|
|
140
|
+
:finite?, :infinite?, :nonzero?,
|
|
117
141
|
:succ, :pred, :next, :abs, :magnitude,
|
|
118
142
|
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
119
143
|
:floor, :ceil, :round, :truncate, :chr,
|
|
120
144
|
:inspect, :-@, :+@, :~, :to_r, :to_c
|
|
121
145
|
].freeze
|
|
122
146
|
FLOAT_UNARY = Set[
|
|
123
|
-
:zero?, :positive?, :negative?,
|
|
124
|
-
:nan?, :finite?, :infinite?,
|
|
147
|
+
:zero?, :positive?, :negative?, :nonzero?,
|
|
148
|
+
:nan?, :finite?, :infinite?, :integer?,
|
|
125
149
|
:abs, :magnitude, :floor, :ceil, :round, :truncate,
|
|
126
150
|
:next_float, :prev_float,
|
|
127
151
|
:to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
|
|
152
|
+
# `numerator` / `denominator` expose the rational
|
|
153
|
+
# decomposition of the float (`2.5.numerator → 5`,
|
|
154
|
+
# `.denominator → 2`) — pure arithmetic, the Float siblings
|
|
155
|
+
# of the already-folded Rational accessors. The non-finite
|
|
156
|
+
# edges stay sound: `Infinity.numerator → Infinity` /
|
|
157
|
+
# `.denominator → 1` fold to the same value Ruby returns, and
|
|
158
|
+
# `NaN.numerator → NaN` is declined by `foldable_constant_value?`.
|
|
159
|
+
:numerator, :denominator,
|
|
160
|
+
# `arg` / `angle` / `phase` (aliases) return the complex
|
|
161
|
+
# argument of the real number: `0` for `self >= 0`, `Math::PI`
|
|
162
|
+
# for `self < 0`. Pure sign test, deterministic; a NaN
|
|
163
|
+
# receiver yields NaN which `foldable_constant_value?` declines.
|
|
164
|
+
:arg, :angle, :phase,
|
|
128
165
|
:inspect, :-@, :+@
|
|
129
166
|
].freeze
|
|
130
167
|
STRING_UNARY = Set[
|
|
@@ -133,12 +170,21 @@ module Rigor
|
|
|
133
170
|
:empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
|
|
134
171
|
:to_s, :to_str, :to_sym, :intern,
|
|
135
172
|
:to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
|
|
136
|
-
:sum, :inspect
|
|
173
|
+
:sum, :inspect,
|
|
174
|
+
# `shellescape` is the String-receiver twin of the already-folded
|
|
175
|
+
# `Shellwords.escape` — deterministic shell-quoting, no global
|
|
176
|
+
# state. The `shellwords` library is loaded process-wide via
|
|
177
|
+
# `shellwords_folding`, so the method is always defined here.
|
|
178
|
+
:shellescape
|
|
137
179
|
].freeze
|
|
138
180
|
SYMBOL_UNARY = Set[
|
|
139
181
|
:to_s, :to_sym, :to_proc, :length, :size,
|
|
140
182
|
:empty?, :upcase, :downcase, :capitalize,
|
|
141
|
-
:swapcase, :succ, :next, :inspect
|
|
183
|
+
:swapcase, :succ, :next, :inspect,
|
|
184
|
+
# `name` (the frozen-string accessor), `id2name` (alias of
|
|
185
|
+
# `to_s`), and `intern` (alias of `to_sym`) are pure reads of the
|
|
186
|
+
# symbol's text — siblings of the already-folded `to_s` / `to_sym`.
|
|
187
|
+
:name, :id2name, :intern
|
|
142
188
|
].freeze
|
|
143
189
|
BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
|
|
144
190
|
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect].freeze
|
|
@@ -377,26 +423,8 @@ module Rigor
|
|
|
377
423
|
end
|
|
378
424
|
|
|
379
425
|
def try_fold_unary_set(receiver_values, method_name)
|
|
380
|
-
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
string_lift = try_fold_string_array_unary(receiver_values, method_name)
|
|
384
|
-
return string_lift if string_lift
|
|
385
|
-
|
|
386
|
-
pathname_lift = try_fold_pathname_unary(receiver_values, method_name)
|
|
387
|
-
return pathname_lift if pathname_lift
|
|
388
|
-
|
|
389
|
-
regexp_lift = try_fold_regexp_array_unary(receiver_values, method_name)
|
|
390
|
-
return regexp_lift if regexp_lift
|
|
391
|
-
|
|
392
|
-
set_lift = try_fold_set_array_unary(receiver_values, method_name)
|
|
393
|
-
return set_lift if set_lift
|
|
394
|
-
|
|
395
|
-
integer_lift = try_fold_integer_array_unary(receiver_values, method_name)
|
|
396
|
-
return integer_lift if integer_lift
|
|
397
|
-
|
|
398
|
-
numeric_lift = try_fold_numeric_array_unary(receiver_values, method_name)
|
|
399
|
-
return numeric_lift if numeric_lift
|
|
426
|
+
special = try_fold_unary_special(receiver_values, method_name)
|
|
427
|
+
return special if special
|
|
400
428
|
|
|
401
429
|
# Type-level allow check on every receiver. If one member's
|
|
402
430
|
# type does not have the method in its allow list (e.g.
|
|
@@ -411,6 +439,23 @@ module Rigor
|
|
|
411
439
|
end
|
|
412
440
|
build_constant_type(results, source: receiver_values)
|
|
413
441
|
end
|
|
442
|
+
|
|
443
|
+
# The carrier-specific unary lifts — Range-to-Tuple, the
|
|
444
|
+
# Array-returning String / Pathname / Regexp / Set / Integer /
|
|
445
|
+
# Numeric folds — that produce a precise structural type before
|
|
446
|
+
# the generic scalar `invoke_unary` path. The first match wins;
|
|
447
|
+
# nil means none applied and the caller falls through to the
|
|
448
|
+
# scalar allow-list path.
|
|
449
|
+
def try_fold_unary_special(receiver_values, method_name)
|
|
450
|
+
try_fold_range_constant_unary(receiver_values, method_name) ||
|
|
451
|
+
try_fold_string_array_unary(receiver_values, method_name) ||
|
|
452
|
+
try_fold_pathname_unary(receiver_values, method_name) ||
|
|
453
|
+
try_fold_pathname_array_unary(receiver_values, method_name) ||
|
|
454
|
+
try_fold_regexp_array_unary(receiver_values, method_name) ||
|
|
455
|
+
try_fold_set_array_unary(receiver_values, method_name) ||
|
|
456
|
+
try_fold_integer_array_unary(receiver_values, method_name) ||
|
|
457
|
+
try_fold_numeric_array_unary(receiver_values, method_name)
|
|
458
|
+
end
|
|
414
459
|
# v0.0.7 — `Constant<Range>#to_a` and the no-arg
|
|
415
460
|
# `first` / `last` / `min` / `max` short-circuit through a
|
|
416
461
|
# Range-specific arm that catalog dispatch cannot reach:
|
|
@@ -427,10 +472,13 @@ module Rigor
|
|
|
427
472
|
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
|
|
428
473
|
:sum].freeze
|
|
429
474
|
# 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
|
|
430
|
-
# `take(n)` return the first `n` elements, `last(n)` the final `n
|
|
431
|
-
#
|
|
432
|
-
#
|
|
433
|
-
|
|
475
|
+
# `take(n)` return the first `n` elements, `last(n)` the final `n`,
|
|
476
|
+
# and `min(n)` / `max(n)` the n smallest / largest (for an ascending
|
|
477
|
+
# integer range `min(n) == first(n)` and `max(n) == last(n).reverse`)
|
|
478
|
+
# — each lifts to a per-position `Tuple[Constant[Integer]…]`. The
|
|
479
|
+
# no-arg `first` / `last` / `min` / `max` stay on the unary path
|
|
480
|
+
# (single Integer endpoint).
|
|
481
|
+
RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take, :min, :max].freeze
|
|
434
482
|
RANGE_TO_A_LIMIT = 16
|
|
435
483
|
private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
|
|
436
484
|
|
|
@@ -509,17 +557,31 @@ module Rigor
|
|
|
509
557
|
|
|
510
558
|
def range_take_tuple(range, method_name, count)
|
|
511
559
|
return nil unless count.is_a?(Integer) && !count.negative?
|
|
512
|
-
# `first(n)`/`last(n)`/`take(n)` materialise at
|
|
513
|
-
# elements; cap that count so a huge `n` (or
|
|
514
|
-
# the Constant. `Range#size`
|
|
560
|
+
# `first(n)`/`last(n)`/`take(n)`/`min(n)`/`max(n)` materialise at
|
|
561
|
+
# most `min(n, size)` elements; cap that count so a huge `n` (or
|
|
562
|
+
# range) never blows up the Constant. `Range#size` and the head/
|
|
563
|
+
# tail projections are O(n) for integer endpoints (no full
|
|
564
|
+
# materialisation).
|
|
515
565
|
return nil if [count, range.size].min > RANGE_TO_A_LIMIT
|
|
516
566
|
|
|
517
|
-
values =
|
|
567
|
+
values = range_head_tail(range, method_name, count)
|
|
518
568
|
return Type::Combinator.tuple_of if values.empty?
|
|
519
569
|
|
|
520
570
|
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
521
571
|
end
|
|
522
572
|
|
|
573
|
+
# The n elements a head/tail projection selects, in Ruby's order.
|
|
574
|
+
# For an ascending integer range `min(n)` is the leading `n`
|
|
575
|
+
# (`first(n)`) and `max(n)` the trailing `n` reversed (descending),
|
|
576
|
+
# so neither needs the full sort `Array#min`/`#max` would do.
|
|
577
|
+
def range_head_tail(range, method_name, count)
|
|
578
|
+
case method_name
|
|
579
|
+
when :last then range.last(count)
|
|
580
|
+
when :max then range.last(count).reverse
|
|
581
|
+
else range.first(count) # :first, :take, :min
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
523
585
|
def try_fold_binary_set(receiver_values, method_name, arg_values)
|
|
524
586
|
range_lift = try_fold_range_constant_binary(receiver_values, method_name, arg_values)
|
|
525
587
|
return range_lift if range_lift
|
|
@@ -542,14 +604,21 @@ module Rigor
|
|
|
542
604
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
543
605
|
end
|
|
544
606
|
# v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
|
|
545
|
-
# `lines` / `split` (no-arg) return a Ruby
|
|
546
|
-
# scalars; `foldable_constant_value?` rejects Array
|
|
607
|
+
# `grapheme_clusters` / `lines` / `split` (no-arg) return a Ruby
|
|
608
|
+
# Array of foldable scalars; `foldable_constant_value?` rejects Array
|
|
547
609
|
# results, so the standard unary path declines. Lift the
|
|
548
610
|
# Array to a per-position `Tuple[Constant…]` directly,
|
|
549
611
|
# capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
|
|
550
612
|
# bounded for long strings. (`codepoints` yields per-character
|
|
551
|
-
# Integer codepoints, the sibling of the byte-valued `bytes
|
|
552
|
-
|
|
613
|
+
# Integer codepoints, the sibling of the byte-valued `bytes`;
|
|
614
|
+
# `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
|
|
615
|
+
# `shellsplit` is the String-receiver twin of the already-folded
|
|
616
|
+
# `Shellwords.split` — lifts the token Array to a Tuple. Raises
|
|
617
|
+
# `ArgumentError` on unmatched quotes, which `try_fold_string_array_unary`
|
|
618
|
+
# rescues to nil (RBS tier widens). `shellwords` is loaded process-wide
|
|
619
|
+
# via `shellwords_folding`.
|
|
620
|
+
STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters,
|
|
621
|
+
:lines, :split, :shellsplit].freeze
|
|
553
622
|
# `partition` / `rpartition` always return a fixed 3-element
|
|
554
623
|
# `[head, separator, tail]` Array whose members are substrings of
|
|
555
624
|
# the receiver (bounded by the input), so they lift to a precise
|
|
@@ -613,10 +682,26 @@ module Rigor
|
|
|
613
682
|
].freeze
|
|
614
683
|
PATHNAME_PURE_BINARY = Set[
|
|
615
684
|
:+, :join, :sub_ext, :<=>, :==, :eql?, :===,
|
|
616
|
-
:relative_path_from
|
|
685
|
+
:relative_path_from,
|
|
686
|
+
# `/` is the exact alias of `+` (`def /(other) = self + other`),
|
|
687
|
+
# the idiomatic path-join operator (`dir / "file"`). `basename`'s
|
|
688
|
+
# 1-arg suffix-stripping form (`path.basename(".rb")` → the stem)
|
|
689
|
+
# is the binary sibling of the already-folded no-arg `basename` —
|
|
690
|
+
# both are pure `@path` string manipulation, no filesystem read.
|
|
691
|
+
:/, :basename
|
|
617
692
|
].freeze
|
|
618
693
|
private_constant :PATHNAME_PURE_UNARY, :PATHNAME_PURE_BINARY
|
|
619
694
|
|
|
695
|
+
# `Constant<Pathname>#split` returns the fixed 2-element
|
|
696
|
+
# `[dirname, basename]` pair (both Pathname), the path-string
|
|
697
|
+
# split of `File.split`. Lifted to `Tuple[Constant[Pathname],
|
|
698
|
+
# Constant[Pathname]]`. Filesystem-independent — reads only
|
|
699
|
+
# `@path` — so it is deterministic at fold time, the
|
|
700
|
+
# Array-returning sibling of the scalar `basename` / `dirname`
|
|
701
|
+
# folds (which `try_fold_pathname_unary` already covers).
|
|
702
|
+
PATHNAME_ARRAY_UNARY_METHODS = Set[:split].freeze
|
|
703
|
+
private_constant :PATHNAME_ARRAY_UNARY_METHODS
|
|
704
|
+
|
|
620
705
|
def try_fold_pathname_unary(receiver_values, method_name)
|
|
621
706
|
return nil unless PATHNAME_PURE_UNARY.include?(method_name)
|
|
622
707
|
return nil unless receiver_values.size == 1
|
|
@@ -649,6 +734,22 @@ module Rigor
|
|
|
649
734
|
nil
|
|
650
735
|
end
|
|
651
736
|
|
|
737
|
+
# `Constant<Pathname>#split` — lift the `[dirname, basename]`
|
|
738
|
+
# Pathname pair to a Tuple[Constant[Pathname], Constant[Pathname]].
|
|
739
|
+
# Pure path-string manipulation (no filesystem read); both
|
|
740
|
+
# elements are Pathname, a foldable Constant class.
|
|
741
|
+
def try_fold_pathname_array_unary(receiver_values, method_name)
|
|
742
|
+
return nil unless PATHNAME_ARRAY_UNARY_METHODS.include?(method_name)
|
|
743
|
+
return nil unless receiver_values.size == 1
|
|
744
|
+
|
|
745
|
+
receiver = receiver_values.first
|
|
746
|
+
return nil unless receiver.is_a?(Pathname)
|
|
747
|
+
|
|
748
|
+
lift_array_result(receiver.split)
|
|
749
|
+
rescue StandardError
|
|
750
|
+
nil
|
|
751
|
+
end
|
|
752
|
+
|
|
652
753
|
def try_fold_string_array_unary(receiver_values, method_name)
|
|
653
754
|
return nil unless STRING_ARRAY_UNARY_METHODS.include?(method_name)
|
|
654
755
|
return nil unless receiver_values.size == 1
|
|
@@ -1490,6 +1591,7 @@ module Rigor
|
|
|
1490
1591
|
when nil then NIL_BINARY
|
|
1491
1592
|
when Rational then RATIONAL_BINARY
|
|
1492
1593
|
when Complex then COMPLEX_BINARY
|
|
1594
|
+
when ::Set then SET_BINARY
|
|
1493
1595
|
else Set.new
|
|
1494
1596
|
end
|
|
1495
1597
|
end
|