rigortype 0.0.1

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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +373 -0
  3. data/README.md +152 -0
  4. data/exe/rigor +9 -0
  5. data/lib/rigor/analysis/check_rules.rb +503 -0
  6. data/lib/rigor/analysis/diagnostic.rb +35 -0
  7. data/lib/rigor/analysis/fact_store.rb +133 -0
  8. data/lib/rigor/analysis/result.rb +29 -0
  9. data/lib/rigor/analysis/runner.rb +119 -0
  10. data/lib/rigor/ast/type_node.rb +41 -0
  11. data/lib/rigor/ast.rb +22 -0
  12. data/lib/rigor/cli/type_of_command.rb +160 -0
  13. data/lib/rigor/cli/type_of_renderer.rb +88 -0
  14. data/lib/rigor/cli/type_scan_command.rb +160 -0
  15. data/lib/rigor/cli/type_scan_renderer.rb +165 -0
  16. data/lib/rigor/cli/type_scan_report.rb +32 -0
  17. data/lib/rigor/cli.rb +195 -0
  18. data/lib/rigor/configuration.rb +49 -0
  19. data/lib/rigor/environment/class_registry.rb +141 -0
  20. data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
  21. data/lib/rigor/environment/rbs_loader.rb +244 -0
  22. data/lib/rigor/environment.rb +177 -0
  23. data/lib/rigor/inference/acceptance.rb +444 -0
  24. data/lib/rigor/inference/block_parameter_binder.rb +198 -0
  25. data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
  26. data/lib/rigor/inference/coverage_scanner.rb +85 -0
  27. data/lib/rigor/inference/expression_typer.rb +831 -0
  28. data/lib/rigor/inference/fallback.rb +35 -0
  29. data/lib/rigor/inference/fallback_tracer.rb +64 -0
  30. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
  31. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
  32. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
  33. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
  34. data/lib/rigor/inference/method_dispatcher.rb +213 -0
  35. data/lib/rigor/inference/method_parameter_binder.rb +257 -0
  36. data/lib/rigor/inference/multi_target_binder.rb +143 -0
  37. data/lib/rigor/inference/narrowing.rb +1008 -0
  38. data/lib/rigor/inference/rbs_type_translator.rb +219 -0
  39. data/lib/rigor/inference/scope_indexer.rb +468 -0
  40. data/lib/rigor/inference/statement_evaluator.rb +1017 -0
  41. data/lib/rigor/rbs_extended.rb +98 -0
  42. data/lib/rigor/scope.rb +340 -0
  43. data/lib/rigor/source/node_locator.rb +104 -0
  44. data/lib/rigor/source/node_walker.rb +37 -0
  45. data/lib/rigor/source.rb +15 -0
  46. data/lib/rigor/testing.rb +65 -0
  47. data/lib/rigor/trinary.rb +108 -0
  48. data/lib/rigor/type/accepts_result.rb +109 -0
  49. data/lib/rigor/type/bot.rb +57 -0
  50. data/lib/rigor/type/combinator.rb +148 -0
  51. data/lib/rigor/type/constant.rb +90 -0
  52. data/lib/rigor/type/dynamic.rb +60 -0
  53. data/lib/rigor/type/hash_shape.rb +246 -0
  54. data/lib/rigor/type/nominal.rb +83 -0
  55. data/lib/rigor/type/singleton.rb +65 -0
  56. data/lib/rigor/type/top.rb +56 -0
  57. data/lib/rigor/type/tuple.rb +84 -0
  58. data/lib/rigor/type/union.rb +65 -0
  59. data/lib/rigor/type.rb +23 -0
  60. data/lib/rigor/version.rb +5 -0
  61. data/lib/rigor.rb +29 -0
  62. data/sig/rigor/analysis/fact_store.rbs +51 -0
  63. data/sig/rigor/ast.rbs +11 -0
  64. data/sig/rigor/environment.rbs +59 -0
  65. data/sig/rigor/inference.rbs +151 -0
  66. data/sig/rigor/rbs_extended.rbs +22 -0
  67. data/sig/rigor/scope.rbs +49 -0
  68. data/sig/rigor/source.rbs +20 -0
  69. data/sig/rigor/testing.rbs +9 -0
  70. data/sig/rigor/trinary.rbs +29 -0
  71. data/sig/rigor/type.rbs +171 -0
  72. data/sig/rigor.rbs +70 -0
  73. metadata +260 -0
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ require_relative "../type"
6
+ require_relative "../inference/rbs_type_translator"
7
+ require_relative "rbs_hierarchy"
8
+
9
+ module Rigor
10
+ class Environment
11
+ # Loads RBS class declarations and method definitions from disk and
12
+ # exposes them to the inference engine in a small, stable surface.
13
+ #
14
+ # Slice 4 phase 1 only enabled the RBS *core* signatures shipped with
15
+ # the `rbs` gem (`Object`, `Integer`, `String`, `Array`, ...). Phase
16
+ # 2a adds opt-in stdlib library loading (`pathname`, `json`,
17
+ # `tempfile`, ...) and arbitrary-directory signature loading
18
+ # (typically the project's local `sig/` tree). Both are off by
19
+ # default on `RbsLoader.default` so the core-only fast path stays
20
+ # cheap; project-aware loading is opted into through
21
+ # {Environment.for_project} or by constructing a custom loader.
22
+ #
23
+ # The default instance is shared across the process: building the
24
+ # core RBS environment costs hundreds of milliseconds and the data
25
+ # is read-only. The shared instance is frozen, but holds a mutable
26
+ # state hash for lazy memoization of the heavy `RBS::Environment`
27
+ # and `RBS::DefinitionBuilder` -- the user-visible API stays purely
28
+ # functional.
29
+ #
30
+ # See docs/internal-spec/inference-engine.md for the binding contract.
31
+ # rubocop:disable Metrics/ClassLength
32
+ class RbsLoader
33
+ class << self
34
+ def default
35
+ @default ||= new.freeze
36
+ end
37
+
38
+ # Used by tests to discard the cached default loader; production
39
+ # code MUST NOT call this. The shared loader holds a several-MB
40
+ # RBS::Environment, so dropping it during a normal run wastes the
41
+ # cost of rebuilding it.
42
+ def reset_default!
43
+ @default = nil
44
+ end
45
+ end
46
+
47
+ attr_reader :libraries, :signature_paths
48
+
49
+ # @param libraries [Array<String, Symbol>] stdlib library names to
50
+ # load on top of core (e.g., `["pathname", "json"]`). Empty by
51
+ # default. Each entry MUST correspond to a directory under the
52
+ # `rbs` gem's `stdlib/` tree; unknown names are silently dropped
53
+ # on environment build (the underlying `RBS::EnvironmentLoader`
54
+ # raises and we fail-soft).
55
+ # @param signature_paths [Array<String, Pathname>] additional
56
+ # directories of `.rbs` files to load (typically the project's
57
+ # `sig/` tree). Non-existent or non-directory paths are filtered
58
+ # out at build time so the loader stays robust to fixtures and
59
+ # bare repositories.
60
+ def initialize(libraries: [], signature_paths: [])
61
+ @libraries = libraries.map(&:to_s).freeze
62
+ @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
63
+ @state = { env: nil, builder: nil }
64
+ @instance_definition_cache = {}
65
+ @singleton_definition_cache = {}
66
+ @class_known_cache = {}
67
+ @hierarchy = RbsHierarchy.new(self)
68
+ end
69
+
70
+ # Returns true when an RBS class or module declaration with the given
71
+ # name is loaded. Accepts unprefixed or top-level-prefixed names
72
+ # ("Integer" or "::Integer"). Memoized per-name (positive and
73
+ # negative results both cache).
74
+ def class_known?(name)
75
+ key = name.to_s
76
+ return @class_known_cache[key] if @class_known_cache.key?(key)
77
+
78
+ @class_known_cache[key] = compute_class_known(name)
79
+ end
80
+
81
+ # @return [RBS::Definition, nil] the resolved instance definition
82
+ # for `class_name`, or nil when the class is unknown or its
83
+ # definition cannot be built (RBS may raise on broken hierarchies;
84
+ # we fail-soft and return nil so the caller can fall back).
85
+ def instance_definition(class_name)
86
+ key = class_name.to_s
87
+ return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
88
+
89
+ @instance_definition_cache[key] = build_instance_definition(class_name)
90
+ end
91
+
92
+ # @return [RBS::Definition::Method, nil]
93
+ def instance_method(class_name:, method_name:)
94
+ definition = instance_definition(class_name)
95
+ return nil unless definition
96
+
97
+ definition.methods[method_name.to_sym]
98
+ end
99
+
100
+ # @return [RBS::Definition, nil] the resolved singleton (class
101
+ # object) definition for `class_name`. The methods on this
102
+ # definition are the *class methods* of `class_name`, including
103
+ # those inherited from `Class` and `Module` for class types.
104
+ # Returns nil for unknown names and on RBS build errors (fail-soft).
105
+ def singleton_definition(class_name)
106
+ key = class_name.to_s
107
+ return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
108
+
109
+ @singleton_definition_cache[key] = build_singleton_definition(class_name)
110
+ end
111
+
112
+ # @return [RBS::Definition::Method, nil] the class method on
113
+ # `class_name`. For example, `singleton_method(class_name:
114
+ # "Integer", method_name: :sqrt)` returns the definition for
115
+ # `Integer.sqrt`, while `singleton_method(class_name: "Foo",
116
+ # method_name: :new)` returns Class#new for any class type.
117
+ def singleton_method(class_name:, method_name:)
118
+ definition = singleton_definition(class_name)
119
+ return nil unless definition
120
+
121
+ definition.methods[method_name.to_sym]
122
+ end
123
+
124
+ # Slice 4 phase 2d. Returns the class's declared type-parameter
125
+ # names as Symbols (e.g., `[:Elem]` for `Array`, `[:K, :V]` for
126
+ # `Hash`). Used by the dispatcher to build the substitution map
127
+ # from receiver `type_args` into the method's return type. The
128
+ # instance definition is the canonical source because singleton
129
+ # methods (e.g., `Array.new`) parameterize over the same `Elem`
130
+ # as instance methods.
131
+ #
132
+ # Returns an empty array for non-generic classes and for unknown
133
+ # names (the loader stays fail-soft). NOTE: in the `rbs` gem,
134
+ # `RBS::Definition#type_params` returns `Array<Symbol>` directly,
135
+ # not the AST `TypeParam` object (those live on the AST level).
136
+ def class_type_param_names(class_name)
137
+ definition = instance_definition(class_name)
138
+ return [] unless definition
139
+
140
+ definition.type_params.dup
141
+ end
142
+
143
+ def class_ordering(lhs, rhs)
144
+ @hierarchy.class_ordering(lhs, rhs)
145
+ end
146
+
147
+ # Slice A constant-value lookup. Returns the translated
148
+ # `Rigor::Type` for a non-class constant declaration
149
+ # (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or
150
+ # `nil` when no constant entry exists for `name` in the
151
+ # loaded RBS environment. Callers MUST treat the return
152
+ # value as authoritative when present and as "unknown" when
153
+ # nil; the loader does NOT consult the class declarations
154
+ # here — class objects are still resolved through
155
+ # {#class_known?} and `Environment#singleton_for_name`.
156
+ def constant_type(name)
157
+ rbs_name = parse_type_name(name)
158
+ return nil unless rbs_name
159
+
160
+ entry = env.constant_decls[rbs_name]
161
+ return nil unless entry
162
+
163
+ translated = Inference::RbsTypeTranslator.translate(entry.decl.type)
164
+ translated unless translated.is_a?(Type::Bot)
165
+ rescue StandardError
166
+ nil
167
+ end
168
+
169
+ private
170
+
171
+ def env
172
+ @state[:env] ||= build_env
173
+ end
174
+
175
+ def builder
176
+ @state[:builder] ||= RBS::DefinitionBuilder.new(env: env)
177
+ end
178
+
179
+ def build_env
180
+ rbs_loader = RBS::EnvironmentLoader.new
181
+ @libraries.each do |library|
182
+ # Phase 2a deliberately fails-soft on unknown stdlib libraries
183
+ # so a stale `.rigor.yml` (or future config plumbing) does not
184
+ # take down the whole analyzer. Phase 2b will surface this
185
+ # through diagnostics once the configuration layer can name
186
+ # the offending source. The unknown-library check happens at
187
+ # `from_loader` time, not at `add` time, so we have to gate
188
+ # ahead of `add`.
189
+ next unless rbs_loader.has_library?(library: library, version: nil)
190
+
191
+ rbs_loader.add(library: library, version: nil)
192
+ end
193
+ @signature_paths.each do |path|
194
+ rbs_loader.add(path: path) if path.directory?
195
+ end
196
+ RBS::Environment.from_loader(rbs_loader).resolve_type_names
197
+ end
198
+
199
+ def build_instance_definition(class_name)
200
+ rbs_name = parse_type_name(class_name)
201
+ return nil unless rbs_name
202
+ return nil unless env.class_decls.key?(rbs_name)
203
+
204
+ builder.build_instance(rbs_name)
205
+ rescue StandardError
206
+ nil
207
+ end
208
+
209
+ def build_singleton_definition(class_name)
210
+ rbs_name = parse_type_name(class_name)
211
+ return nil unless rbs_name
212
+ return nil unless env.class_decls.key?(rbs_name)
213
+
214
+ builder.build_singleton(rbs_name)
215
+ rescue StandardError
216
+ nil
217
+ end
218
+
219
+ def parse_type_name(name)
220
+ s = name.to_s
221
+ return nil if s.empty?
222
+
223
+ s = "::#{s}" unless s.start_with?("::")
224
+ RBS::TypeName.parse(s)
225
+ rescue StandardError
226
+ nil
227
+ end
228
+
229
+ def compute_class_known(name)
230
+ rbs_name = parse_type_name(name)
231
+ return false unless rbs_name
232
+
233
+ # `RBS::Environment#class_decls` after `resolve_type_names`
234
+ # holds entries for both classes AND modules; the gem unifies
235
+ # them under one map post-resolution. Aliases live in their
236
+ # own table.
237
+ env.class_decls.key?(rbs_name) || env.class_alias_decls.key?(rbs_name)
238
+ rescue StandardError
239
+ false
240
+ end
241
+ end
242
+ # rubocop:enable Metrics/ClassLength
243
+ end
244
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "environment/class_registry"
4
+ require_relative "environment/rbs_loader"
5
+
6
+ module Rigor
7
+ # The engine's view of the type universe outside the current scope.
8
+ # Slice 1 only exposed the class registry; Slice 4 adds the RBS loader,
9
+ # which threads through ExpressionTyper and MethodDispatcher to type
10
+ # constant references and method calls that the literal-typer and
11
+ # constant-folding tiers cannot answer.
12
+ #
13
+ # See docs/internal-spec/inference-engine.md for the binding contract.
14
+ class Environment
15
+ DEFAULT_PROJECT_SIG_DIR = "sig"
16
+ private_constant :DEFAULT_PROJECT_SIG_DIR
17
+
18
+ # Slice A stdlib expansion. Stdlib libraries that
19
+ # `Environment.for_project` loads on top of RBS core unless
20
+ # the caller passes an explicit `libraries:` array. Each
21
+ # entry MUST be a stdlib library name accepted by
22
+ # `RBS::EnvironmentLoader#has_library?`; unknown libraries
23
+ # MUST fail-soft (`RbsLoader#build_env` already filters
24
+ # through `has_library?`). The default set covers the common
25
+ # stdlib surface a Ruby program is likely to import
26
+ # (`pathname`, `optparse`, `json`, `yaml`, `fileutils`,
27
+ # `tempfile`, `uri`, `logger`, `date`) plus the analyzer-
28
+ # adjacent gems shipping their own RBS in this bundle
29
+ # (`prism`, `rbs`). On hosts where one of these libraries is
30
+ # not installed, the loader silently drops it.
31
+ #
32
+ # Callers MAY add to the default by passing
33
+ # `libraries: %w[csv ...]`; the explicit list is appended to
34
+ # `DEFAULT_LIBRARIES` and de-duplicated. Callers that need
35
+ # a strictly RBS-core view MUST construct an `RbsLoader`
36
+ # directly instead of going through `for_project`.
37
+ DEFAULT_LIBRARIES = %w[
38
+ pathname optparse json yaml fileutils tempfile uri logger date
39
+ prism rbs
40
+ ].freeze
41
+
42
+ attr_reader :class_registry, :rbs_loader
43
+
44
+ # @param class_registry [Rigor::Environment::ClassRegistry]
45
+ # @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
46
+ # environment is "RBS-blind"; useful in tests that want to assert
47
+ # how the engine behaves without RBS data. The default Environment
48
+ # wires the shared core loader, which is itself lazy: requesting an
49
+ # environment instance does NOT load RBS until a method or class
50
+ # query actually consults the loader.
51
+ def initialize(class_registry: ClassRegistry.default, rbs_loader: nil)
52
+ @class_registry = class_registry
53
+ @rbs_loader = rbs_loader
54
+ freeze
55
+ end
56
+
57
+ class << self
58
+ def default
59
+ @default ||= new(rbs_loader: RbsLoader.default).freeze
60
+ end
61
+
62
+ # Builds an Environment that consults the project's local
63
+ # signatures and any opt-in stdlib libraries on top of RBS core.
64
+ #
65
+ # @param root [String, Pathname] project root used to auto-detect
66
+ # the default signature path. Defaults to the current working
67
+ # directory.
68
+ # @param libraries [Array<String, Symbol>] additional stdlib
69
+ # libraries to load on top of {DEFAULT_LIBRARIES}. The
70
+ # final list is the union of the two, de-duplicated while
71
+ # preserving order. Pass an empty array (the default) to
72
+ # load only the defaults.
73
+ # @param signature_paths [Array<String, Pathname>, nil] explicit
74
+ # list of `sig/`-style directories. When `nil` (the default),
75
+ # the canonical project layout `<root>/sig` is used if it
76
+ # exists, otherwise no signature path is loaded.
77
+ # @return [Rigor::Environment]
78
+ def for_project(root: Dir.pwd, libraries: [], signature_paths: nil)
79
+ resolved_paths = signature_paths || default_signature_paths(root)
80
+ merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
81
+ loader = RbsLoader.new(libraries: merged_libraries, signature_paths: resolved_paths)
82
+ new(rbs_loader: loader)
83
+ end
84
+
85
+ private
86
+
87
+ def default_signature_paths(root)
88
+ sig = Pathname(root) / DEFAULT_PROJECT_SIG_DIR
89
+ sig.directory? ? [sig] : []
90
+ end
91
+ end
92
+
93
+ # Resolves a constant name to a Rigor::Type::Nominal (the *instance*
94
+ # type carrier). Consults the static class registry first (cheap,
95
+ # hardcoded), then falls back to the RBS loader. Returns nil when
96
+ # the name is unknown to both.
97
+ #
98
+ # NOTE: This is the construction helper for "an instance of class
99
+ # `Foo`". For "the class object `Foo` itself" (the value of the
100
+ # constant), use {#singleton_for_name} instead.
101
+ def nominal_for_name(name)
102
+ registered = class_registry.nominal_for_name(name)
103
+ return registered if registered
104
+
105
+ class_known_in_rbs?(name) ? Type::Combinator.nominal_of(name.to_s) : nil
106
+ end
107
+
108
+ # Resolves a constant name to a Rigor::Type::Singleton (the *class
109
+ # object* carrier). The expression `Foo` evaluates to the class
110
+ # object, whose RBS type is `singleton(Foo)` -- this method is the
111
+ # corresponding Rigor construction helper.
112
+ #
113
+ # The lookup uses the same registry/RBS chain as {#nominal_for_name}
114
+ # so a class is either known to both queries or to neither.
115
+ def singleton_for_name(name)
116
+ return nil unless class_known?(name)
117
+
118
+ Type::Combinator.singleton_of(name.to_s)
119
+ end
120
+
121
+ # Slice A constant-value lookup. Returns the translated
122
+ # `Rigor::Type` for an RBS-declared **non-class** constant
123
+ # (`Rigor::Analysis::FactStore::BUCKETS: Array[Symbol]`,
124
+ # `Rigor::Configuration::DEFAULT_PATH: String`, ...) or `nil`
125
+ # when no RBS constant declaration covers `name`. This is the
126
+ # value-bearing counterpart of {#singleton_for_name}, which
127
+ # only resolves names that name a class or module. Callers
128
+ # that need to type a `Prism::ConstantReadNode`/
129
+ # `Prism::ConstantPathNode` MUST consult {#singleton_for_name}
130
+ # first and fall through to this query when the constant is
131
+ # not a class.
132
+ def constant_for_name(name)
133
+ return nil if rbs_loader.nil?
134
+
135
+ rbs_loader.constant_type(name.to_s)
136
+ end
137
+
138
+ # Returns true when the constant name is known to either the static
139
+ # registry or the RBS loader. Useful for callers that only need a
140
+ # presence check without materialising a type carrier.
141
+ def class_known?(name)
142
+ return true if class_registry.nominal_for_name(name)
143
+
144
+ class_known_in_rbs?(name)
145
+ end
146
+
147
+ # Compares two class/module names using analyzer-owned class data.
148
+ # Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
149
+ # `:unknown`. The static registry handles built-ins cheaply; the RBS
150
+ # loader handles project/stdlib classes without relying on host Ruby
151
+ # constants being loaded.
152
+ def class_ordering(lhs, rhs)
153
+ lhs = normalize_class_name(lhs)
154
+ rhs = normalize_class_name(rhs)
155
+ return :equal if lhs == rhs
156
+
157
+ registry_result = class_registry.class_ordering(lhs, rhs)
158
+ return registry_result unless registry_result == :unknown
159
+
160
+ return :unknown unless rbs_loader
161
+
162
+ rbs_loader.class_ordering(lhs, rhs)
163
+ end
164
+
165
+ private
166
+
167
+ def class_known_in_rbs?(name)
168
+ return false unless rbs_loader
169
+
170
+ rbs_loader.class_known?(name)
171
+ end
172
+
173
+ def normalize_class_name(name)
174
+ name.to_s.delete_prefix("::")
175
+ end
176
+ end
177
+ end