rigortype 0.0.8 → 0.1.0
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 +234 -22
- data/data/builtins/ruby_core/encoding.yml +210 -0
- data/data/builtins/ruby_core/exception.yml +641 -0
- data/data/builtins/ruby_core/numeric.yml +3 -2
- data/data/builtins/ruby_core/proc.yml +731 -0
- data/data/builtins/ruby_core/random.yml +166 -0
- data/data/builtins/ruby_core/re.yml +689 -0
- data/data/builtins/ruby_core/struct.yml +449 -0
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +199 -4
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
- data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
- data/lib/rigor/cache/rbs_constant_table.rb +15 -51
- data/lib/rigor/cache/rbs_descriptor.rb +55 -0
- data/lib/rigor/cache/rbs_environment.rb +52 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +81 -15
- data/lib/rigor/cli.rb +45 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +220 -32
- data/lib/rigor/environment.rb +11 -2
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +179 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
- data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
- data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
- data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
- data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
- data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
- data/lib/rigor/inference/expression_typer.rb +110 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +134 -144
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +103 -0
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/type/combinator.rb +72 -0
- data/lib/rigor/type/refined.rb +50 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +13 -0
- data/sig/rigor/environment.rbs +7 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +3 -1
- metadata +38 -1
|
@@ -42,9 +42,29 @@ module Rigor
|
|
|
42
42
|
def reset_default!
|
|
43
43
|
@default = nil
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
# Builds an `RBS::Environment` from explicit `libraries` and
|
|
47
|
+
# `signature_paths`. Stateless surface so the v0.0.9
|
|
48
|
+
# {Cache::RbsEnvironment} producer can build an env on cache
|
|
49
|
+
# miss without holding a loader instance, and the
|
|
50
|
+
# instance-side {#build_env} delegates here so the
|
|
51
|
+
# implementation stays single-rooted.
|
|
52
|
+
def build_env_for(libraries:, signature_paths:)
|
|
53
|
+
rbs_loader = RBS::EnvironmentLoader.new
|
|
54
|
+
libraries.each do |library|
|
|
55
|
+
next unless rbs_loader.has_library?(library: library, version: nil)
|
|
56
|
+
|
|
57
|
+
rbs_loader.add(library: library, version: nil)
|
|
58
|
+
end
|
|
59
|
+
signature_paths.each do |path|
|
|
60
|
+
path = Pathname(path) unless path.is_a?(Pathname)
|
|
61
|
+
rbs_loader.add(path: path) if path.directory?
|
|
62
|
+
end
|
|
63
|
+
RBS::Environment.from_loader(rbs_loader).resolve_type_names
|
|
64
|
+
end
|
|
45
65
|
end
|
|
46
66
|
|
|
47
|
-
attr_reader :libraries, :signature_paths
|
|
67
|
+
attr_reader :libraries, :signature_paths, :cache_store
|
|
48
68
|
|
|
49
69
|
# @param libraries [Array<String, Symbol>] stdlib library names to
|
|
50
70
|
# load on top of core (e.g., `["pathname", "json"]`). Empty by
|
|
@@ -57,9 +77,16 @@ module Rigor
|
|
|
57
77
|
# `sig/` tree). Non-existent or non-directory paths are filtered
|
|
58
78
|
# out at build time so the loader stays robust to fixtures and
|
|
59
79
|
# bare repositories.
|
|
60
|
-
|
|
80
|
+
# @param cache_store [Rigor::Cache::Store, nil] the persistent
|
|
81
|
+
# cache the loader consults for translated constant lookups
|
|
82
|
+
# (and, in later v0.0.9 slices, other Marshal-clean
|
|
83
|
+
# reflection artefacts). Pass `nil` (the default) to skip
|
|
84
|
+
# the cache entirely; the runner threads its own Store
|
|
85
|
+
# through here when caching is enabled.
|
|
86
|
+
def initialize(libraries: [], signature_paths: [], cache_store: nil)
|
|
61
87
|
@libraries = libraries.map(&:to_s).freeze
|
|
62
88
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
89
|
+
@cache_store = cache_store
|
|
63
90
|
@state = { env: nil, builder: nil }
|
|
64
91
|
@instance_definition_cache = {}
|
|
65
92
|
@singleton_definition_cache = {}
|
|
@@ -71,22 +98,69 @@ module Rigor
|
|
|
71
98
|
# name is loaded. Accepts unprefixed or top-level-prefixed names
|
|
72
99
|
# ("Integer" or "::Integer"). Memoized per-name (positive and
|
|
73
100
|
# negative results both cache).
|
|
101
|
+
#
|
|
102
|
+
# When `cache_store` is set, the loader fetches the entire set of
|
|
103
|
+
# known class / module / alias names once (per process) through
|
|
104
|
+
# {Cache::RbsKnownClassNames.fetch} and answers `class_known?`
|
|
105
|
+
# from the in-memory Set. Cold runs pay a single env walk and
|
|
106
|
+
# persist the result; warm runs (and a separate loader sharing
|
|
107
|
+
# the same Store) skip the env walk entirely.
|
|
74
108
|
def class_known?(name)
|
|
75
109
|
key = name.to_s
|
|
76
110
|
return @class_known_cache[key] if @class_known_cache.key?(key)
|
|
77
111
|
|
|
78
|
-
@class_known_cache[key] =
|
|
112
|
+
@class_known_cache[key] = if cache_store
|
|
113
|
+
cached_class_known(name)
|
|
114
|
+
else
|
|
115
|
+
compute_class_known(name)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Yields every known class / module / alias name (top-level
|
|
120
|
+
# prefixed) currently loaded into the environment. The cache
|
|
121
|
+
# producer that materialises the known-name set uses this so
|
|
122
|
+
# it never recurses back through {#class_known?}.
|
|
123
|
+
def each_known_class_name
|
|
124
|
+
return enum_for(:each_known_class_name) unless block_given?
|
|
125
|
+
|
|
126
|
+
env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
127
|
+
env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
128
|
+
rescue ::RBS::BaseError
|
|
129
|
+
# fail-soft: a broken RBS environment yields no names.
|
|
130
|
+
# Analyzer-internal errors (NameError, NoMethodError,
|
|
131
|
+
# LoadError) are NOT swallowed — those are bugs and
|
|
132
|
+
# must surface so they don't hide silently the way the
|
|
133
|
+
# v0.0.9 cache `Cache::Descriptor` regression did.
|
|
79
134
|
end
|
|
80
135
|
|
|
81
136
|
# @return [RBS::Definition, nil] the resolved instance definition
|
|
82
137
|
# for `class_name`, or nil when the class is unknown or its
|
|
83
138
|
# definition cannot be built (RBS may raise on broken hierarchies;
|
|
84
139
|
# we fail-soft and return nil so the caller can fall back).
|
|
140
|
+
#
|
|
141
|
+
# When `cache_store` is set, the loader fetches the per-class
|
|
142
|
+
# definition through {Cache::RbsInstanceDefinitions.fetch} so
|
|
143
|
+
# subsequent runs (and other loaders sharing the same Store)
|
|
144
|
+
# skip the `RBS::DefinitionBuilder.build_instance` step.
|
|
145
|
+
# In-memory `@instance_definition_cache` keeps the per-process
|
|
146
|
+
# short-circuit on top.
|
|
85
147
|
def instance_definition(class_name)
|
|
86
148
|
key = class_name.to_s
|
|
87
149
|
return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
|
|
88
150
|
|
|
89
|
-
@instance_definition_cache[key] =
|
|
151
|
+
@instance_definition_cache[key] = if cache_store
|
|
152
|
+
cached_instance_definition(class_name)
|
|
153
|
+
else
|
|
154
|
+
build_instance_definition(class_name)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Public uncached accessor used by the cache producer
|
|
159
|
+
# ({Rigor::Cache::RbsInstanceDefinitions}). Avoids the
|
|
160
|
+
# `private_method_called` round-trip a `loader.send(...)`
|
|
161
|
+
# callsite would require.
|
|
162
|
+
def uncached_instance_definition(class_name)
|
|
163
|
+
build_instance_definition(class_name)
|
|
90
164
|
end
|
|
91
165
|
|
|
92
166
|
# @return [RBS::Definition::Method, nil]
|
|
@@ -102,11 +176,25 @@ module Rigor
|
|
|
102
176
|
# definition are the *class methods* of `class_name`, including
|
|
103
177
|
# those inherited from `Class` and `Module` for class types.
|
|
104
178
|
# Returns nil for unknown names and on RBS build errors (fail-soft).
|
|
179
|
+
#
|
|
180
|
+
# When `cache_store` is set, the loader fetches the per-class
|
|
181
|
+
# singleton definition through
|
|
182
|
+
# {Cache::RbsSingletonDefinitions.fetch}; the same caching
|
|
183
|
+
# discipline as {#instance_definition}.
|
|
105
184
|
def singleton_definition(class_name)
|
|
106
185
|
key = class_name.to_s
|
|
107
186
|
return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
|
|
108
187
|
|
|
109
|
-
@singleton_definition_cache[key] =
|
|
188
|
+
@singleton_definition_cache[key] = if cache_store
|
|
189
|
+
cached_singleton_definition(class_name)
|
|
190
|
+
else
|
|
191
|
+
build_singleton_definition(class_name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Public uncached accessor used by the cache producer.
|
|
196
|
+
def uncached_singleton_definition(class_name)
|
|
197
|
+
build_singleton_definition(class_name)
|
|
110
198
|
end
|
|
111
199
|
|
|
112
200
|
# @return [RBS::Definition::Method, nil] the class method on
|
|
@@ -133,7 +221,19 @@ module Rigor
|
|
|
133
221
|
# names (the loader stays fail-soft). NOTE: in the `rbs` gem,
|
|
134
222
|
# `RBS::Definition#type_params` returns `Array<Symbol>` directly,
|
|
135
223
|
# not the AST `TypeParam` object (those live on the AST level).
|
|
224
|
+
#
|
|
225
|
+
# When `cache_store` is set, the loader fetches the entire
|
|
226
|
+
# type-parameter-name table once (per process) through
|
|
227
|
+
# {Cache::RbsClassTypeParamNames.fetch} and answers point
|
|
228
|
+
# lookups from it. Cold runs build the table once and persist
|
|
229
|
+
# it; warm runs (and a separate loader sharing the same Store)
|
|
230
|
+
# skip the env walk entirely.
|
|
136
231
|
def class_type_param_names(class_name)
|
|
232
|
+
if cache_store
|
|
233
|
+
key = class_name.to_s.delete_prefix("::")
|
|
234
|
+
return type_param_names_table.fetch(key, []).dup
|
|
235
|
+
end
|
|
236
|
+
|
|
137
237
|
definition = instance_definition(class_name)
|
|
138
238
|
return [] unless definition
|
|
139
239
|
|
|
@@ -151,10 +251,25 @@ module Rigor
|
|
|
151
251
|
# should keep using {#constant_type} for point lookups.
|
|
152
252
|
def constant_names
|
|
153
253
|
env.constant_decls.keys.map(&:to_s)
|
|
154
|
-
rescue
|
|
254
|
+
rescue ::RBS::BaseError
|
|
155
255
|
[]
|
|
156
256
|
end
|
|
157
257
|
|
|
258
|
+
# Yields `(name, entry)` for every RBS constant declaration
|
|
259
|
+
# currently loaded into the environment. The cache producer
|
|
260
|
+
# uses this to materialise the constant-type table without
|
|
261
|
+
# going back through {#constant_type} (which would recurse
|
|
262
|
+
# back into the cache when `cache_store` is set).
|
|
263
|
+
def each_constant_decl
|
|
264
|
+
return enum_for(:each_constant_decl) unless block_given?
|
|
265
|
+
|
|
266
|
+
env.constant_decls.each do |rbs_name, entry|
|
|
267
|
+
yield rbs_name.to_s, entry
|
|
268
|
+
end
|
|
269
|
+
rescue ::RBS::BaseError
|
|
270
|
+
# fail-soft: a broken RBS environment yields no entries.
|
|
271
|
+
end
|
|
272
|
+
|
|
158
273
|
# Slice A constant-value lookup. Returns the translated
|
|
159
274
|
# `Rigor::Type` for a non-class constant declaration
|
|
160
275
|
# (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or
|
|
@@ -164,23 +279,112 @@ module Rigor
|
|
|
164
279
|
# nil; the loader does NOT consult the class declarations
|
|
165
280
|
# here — class objects are still resolved through
|
|
166
281
|
# {#class_known?} and `Environment#singleton_for_name`.
|
|
282
|
+
#
|
|
283
|
+
# When `cache_store` is set, the loader fetches the entire
|
|
284
|
+
# translated constant table once (per process) through
|
|
285
|
+
# {Cache::RbsConstantTable.fetch} and answers point lookups
|
|
286
|
+
# from it. Cold runs pay the translation cost up-front and
|
|
287
|
+
# write the result to disk; warm runs skip the translation
|
|
288
|
+
# entirely and pay only a `Marshal.load` of the table.
|
|
167
289
|
def constant_type(name)
|
|
168
290
|
rbs_name = parse_type_name(name)
|
|
169
291
|
return nil unless rbs_name
|
|
170
292
|
|
|
293
|
+
if cache_store
|
|
294
|
+
constant_type_table[rbs_name.to_s]
|
|
295
|
+
else
|
|
296
|
+
translate_constant_decl(rbs_name)
|
|
297
|
+
end
|
|
298
|
+
rescue ::RBS::BaseError
|
|
299
|
+
nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
private
|
|
303
|
+
|
|
304
|
+
def constant_type_table
|
|
305
|
+
@constant_type_table ||= begin
|
|
306
|
+
require_relative "../cache/rbs_constant_table"
|
|
307
|
+
Cache::RbsConstantTable.fetch(loader: self, store: cache_store)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def known_class_names_set
|
|
312
|
+
@known_class_names_set ||= begin
|
|
313
|
+
require_relative "../cache/rbs_known_class_names"
|
|
314
|
+
Cache::RbsKnownClassNames.fetch(loader: self, store: cache_store)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def type_param_names_table
|
|
319
|
+
@type_param_names_table ||= begin
|
|
320
|
+
require_relative "../cache/rbs_class_type_param_names"
|
|
321
|
+
Cache::RbsClassTypeParamNames.fetch(loader: self, store: cache_store)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def cached_class_known(name)
|
|
326
|
+
rbs_name = parse_type_name(name)
|
|
327
|
+
return false unless rbs_name
|
|
328
|
+
|
|
329
|
+
known_class_names_set.include?(rbs_name.to_s)
|
|
330
|
+
rescue ::RBS::BaseError
|
|
331
|
+
false
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def translate_constant_decl(rbs_name)
|
|
171
335
|
entry = env.constant_decls[rbs_name]
|
|
172
336
|
return nil unless entry
|
|
173
337
|
|
|
174
338
|
translated = Inference::RbsTypeTranslator.translate(entry.decl.type)
|
|
175
339
|
translated unless translated.is_a?(Type::Bot)
|
|
176
|
-
rescue StandardError
|
|
177
|
-
nil
|
|
178
340
|
end
|
|
179
341
|
|
|
180
|
-
private
|
|
181
|
-
|
|
182
342
|
def env
|
|
183
|
-
@state[:env] ||= build_env
|
|
343
|
+
@state[:env] ||= cache_store ? cached_env : build_env
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def cached_env
|
|
347
|
+
require_relative "../cache/rbs_environment"
|
|
348
|
+
Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Per-process Hash<String, RBS::Definition> for the instance
|
|
352
|
+
# side. Loaded once on first miss through the
|
|
353
|
+
# {Cache::RbsInstanceDefinitions} producer (single Marshal
|
|
354
|
+
# blob); subsequent calls are pure Hash lookups. Cold runs
|
|
355
|
+
# build every known class once and persist; warm runs (and
|
|
356
|
+
# other loaders sharing the same Store) skip the
|
|
357
|
+
# `RBS::DefinitionBuilder.build_instance` work entirely.
|
|
358
|
+
def cached_instance_definition(class_name)
|
|
359
|
+
instance_definitions_table[normalise_class_key(class_name)]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def instance_definitions_table
|
|
363
|
+
@state[:instance_definitions_table] ||= begin
|
|
364
|
+
require_relative "../cache/rbs_instance_definitions"
|
|
365
|
+
Cache::RbsInstanceDefinitions.fetch(loader: self, store: cache_store)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def cached_singleton_definition(class_name)
|
|
370
|
+
singleton_definitions_table[normalise_class_key(class_name)]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def singleton_definitions_table
|
|
374
|
+
@state[:singleton_definitions_table] ||= begin
|
|
375
|
+
require_relative "../cache/rbs_instance_definitions"
|
|
376
|
+
Cache::RbsSingletonDefinitions.fetch(loader: self, store: cache_store)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# The cache producers persist class names in
|
|
381
|
+
# `RBS::TypeName#to_s` form (top-level prefixed
|
|
382
|
+
# `"::Hash"`); plain-name lookups (`"Hash"`) normalise
|
|
383
|
+
# before the Hash query so callers stay agnostic to the
|
|
384
|
+
# prefix.
|
|
385
|
+
def normalise_class_key(class_name)
|
|
386
|
+
s = class_name.to_s
|
|
387
|
+
s.start_with?("::") ? s : "::#{s}"
|
|
184
388
|
end
|
|
185
389
|
|
|
186
390
|
def builder
|
|
@@ -188,23 +392,7 @@ module Rigor
|
|
|
188
392
|
end
|
|
189
393
|
|
|
190
394
|
def build_env
|
|
191
|
-
|
|
192
|
-
@libraries.each do |library|
|
|
193
|
-
# Phase 2a deliberately fails-soft on unknown stdlib libraries
|
|
194
|
-
# so a stale `.rigor.yml` (or future config plumbing) does not
|
|
195
|
-
# take down the whole analyzer. Phase 2b will surface this
|
|
196
|
-
# through diagnostics once the configuration layer can name
|
|
197
|
-
# the offending source. The unknown-library check happens at
|
|
198
|
-
# `from_loader` time, not at `add` time, so we have to gate
|
|
199
|
-
# ahead of `add`.
|
|
200
|
-
next unless rbs_loader.has_library?(library: library, version: nil)
|
|
201
|
-
|
|
202
|
-
rbs_loader.add(library: library, version: nil)
|
|
203
|
-
end
|
|
204
|
-
@signature_paths.each do |path|
|
|
205
|
-
rbs_loader.add(path: path) if path.directory?
|
|
206
|
-
end
|
|
207
|
-
RBS::Environment.from_loader(rbs_loader).resolve_type_names
|
|
395
|
+
self.class.build_env_for(libraries: @libraries, signature_paths: @signature_paths)
|
|
208
396
|
end
|
|
209
397
|
|
|
210
398
|
def build_instance_definition(class_name)
|
|
@@ -213,7 +401,7 @@ module Rigor
|
|
|
213
401
|
return nil unless env.class_decls.key?(rbs_name)
|
|
214
402
|
|
|
215
403
|
builder.build_instance(rbs_name)
|
|
216
|
-
rescue
|
|
404
|
+
rescue ::RBS::BaseError
|
|
217
405
|
nil
|
|
218
406
|
end
|
|
219
407
|
|
|
@@ -223,7 +411,7 @@ module Rigor
|
|
|
223
411
|
return nil unless env.class_decls.key?(rbs_name)
|
|
224
412
|
|
|
225
413
|
builder.build_singleton(rbs_name)
|
|
226
|
-
rescue
|
|
414
|
+
rescue ::RBS::BaseError
|
|
227
415
|
nil
|
|
228
416
|
end
|
|
229
417
|
|
|
@@ -233,7 +421,7 @@ module Rigor
|
|
|
233
421
|
|
|
234
422
|
s = "::#{s}" unless s.start_with?("::")
|
|
235
423
|
RBS::TypeName.parse(s)
|
|
236
|
-
rescue
|
|
424
|
+
rescue ::RBS::BaseError
|
|
237
425
|
nil
|
|
238
426
|
end
|
|
239
427
|
|
|
@@ -246,7 +434,7 @@ module Rigor
|
|
|
246
434
|
# them under one map post-resolution. Aliases live in their
|
|
247
435
|
# own table.
|
|
248
436
|
env.class_decls.key?(rbs_name) || env.class_alias_decls.key?(rbs_name)
|
|
249
|
-
rescue
|
|
437
|
+
rescue ::RBS::BaseError
|
|
250
438
|
false
|
|
251
439
|
end
|
|
252
440
|
end
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -76,11 +76,20 @@ module Rigor
|
|
|
76
76
|
# list of `sig/`-style directories. When `nil` (the default),
|
|
77
77
|
# the canonical project layout `<root>/sig` is used if it
|
|
78
78
|
# exists, otherwise no signature path is loaded.
|
|
79
|
+
# @param cache_store [Rigor::Cache::Store, nil] persistent cache
|
|
80
|
+
# threaded into the underlying {Environment::RbsLoader} so
|
|
81
|
+
# constant lookups (and, in later v0.0.9 slices, other
|
|
82
|
+
# reflection artefacts) consult the cache. Pass `nil` (the
|
|
83
|
+
# default) to skip caching for this environment.
|
|
79
84
|
# @return [Rigor::Environment]
|
|
80
|
-
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil)
|
|
85
|
+
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil)
|
|
81
86
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
82
87
|
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
83
|
-
loader = RbsLoader.new(
|
|
88
|
+
loader = RbsLoader.new(
|
|
89
|
+
libraries: merged_libraries,
|
|
90
|
+
signature_paths: resolved_paths,
|
|
91
|
+
cache_store: cache_store
|
|
92
|
+
)
|
|
84
93
|
new(rbs_loader: loader)
|
|
85
94
|
end
|
|
86
95
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Records a contradiction between two or more flow contributions
|
|
6
|
+
# detected during {Merger.merge}. Carried on {MergeResult#conflicts}
|
|
7
|
+
# so the analyzer / formatter can surface a `:contribution_merge`
|
|
8
|
+
# diagnostic per ADR-2 § "Plugin Contribution Merging".
|
|
9
|
+
#
|
|
10
|
+
# ADR-2 § "Plugin Contribution Merging" rules out first-wins /
|
|
11
|
+
# last-wins behaviour: when contributions conflict, both sources
|
|
12
|
+
# are reported and the merger falls back to the nearest non-
|
|
13
|
+
# conflicting higher-tier (or default) value for the affected
|
|
14
|
+
# `(target, edge, kind)` slot. The conflict object is the
|
|
15
|
+
# carrier of that report.
|
|
16
|
+
#
|
|
17
|
+
# Slice-3 conflict reasons:
|
|
18
|
+
#
|
|
19
|
+
# - `:return_type_collapse` — two return-type contributions
|
|
20
|
+
# intersect to `bot`.
|
|
21
|
+
# - `:exceptional_disagreement` — two contributions assert
|
|
22
|
+
# incompatible non-`nil` exceptional effects.
|
|
23
|
+
# - `:lower_tier_contradiction` — a lower-tier contribution
|
|
24
|
+
# would weaken or contradict a higher-tier proof.
|
|
25
|
+
CONFLICT_VALID_REASONS = %i[
|
|
26
|
+
return_type_collapse
|
|
27
|
+
exceptional_disagreement
|
|
28
|
+
lower_tier_contradiction
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
Conflict = Data.define(:target, :edge, :kind, :reason, :provenances, :message) do
|
|
32
|
+
def initialize(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
|
|
33
|
+
unless CONFLICT_VALID_REASONS.include?(reason)
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"FlowContribution::Conflict reason must be one of " \
|
|
36
|
+
"#{CONFLICT_VALID_REASONS.inspect}, got #{reason.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super(target: target, edge: edge, kind: kind, reason: reason,
|
|
40
|
+
provenances: provenances.dup.freeze, message: message.to_s.dup.freeze)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
"target" => target.to_s,
|
|
46
|
+
"edge" => edge.to_s,
|
|
47
|
+
"kind" => kind.to_s,
|
|
48
|
+
"reason" => reason.to_s,
|
|
49
|
+
"sources" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p.to_s },
|
|
50
|
+
"message" => message
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ADR-7 § "Slice 5-C" — converts the conflict into a
|
|
55
|
+
# `Rigor::Analysis::Diagnostic` for the run result.
|
|
56
|
+
# Carries `source_family: :contribution_merge` so the
|
|
57
|
+
# qualified-rule formatter (slice 5 formatter half,
|
|
58
|
+
# `ef730b2`) prefixes the rule id with
|
|
59
|
+
# `contribution_merge.` and the JSON output side-bands
|
|
60
|
+
# `source_family` + `rule` for plugin attribution.
|
|
61
|
+
#
|
|
62
|
+
# The `rule` identifier is the kebab-case form of the
|
|
63
|
+
# conflict reason (`return_type_collapse` →
|
|
64
|
+
# `return-type-collapse`, etc.) so the qualified rule
|
|
65
|
+
# reads `[contribution_merge.return-type-collapse]` in
|
|
66
|
+
# the standard text stream.
|
|
67
|
+
def to_diagnostic(path:, line:, column:, severity: :error)
|
|
68
|
+
require_relative "../analysis/diagnostic" unless defined?(Rigor::Analysis::Diagnostic)
|
|
69
|
+
Rigor::Analysis::Diagnostic.new(
|
|
70
|
+
path: path,
|
|
71
|
+
line: line,
|
|
72
|
+
column: column,
|
|
73
|
+
message: message,
|
|
74
|
+
severity: severity,
|
|
75
|
+
rule: reason.to_s.tr("_", "-"),
|
|
76
|
+
source_family: :contribution_merge
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Tagged element flattening of a {FlowContribution} bundle —
|
|
6
|
+
# the analyzer-internal representation [ADR-2 § "Flow
|
|
7
|
+
# Contribution Bundle"](../../../docs/adr/2-extension-api.md)
|
|
8
|
+
# routes through the {Merger}.
|
|
9
|
+
#
|
|
10
|
+
# The flattening is **mechanical, deterministic, and round-
|
|
11
|
+
# trippable** with the bundle: every non-empty slot expands
|
|
12
|
+
# into one or more elements keyed by `(target, edge, kind)`,
|
|
13
|
+
# and an array of elements rebuilds an equivalent bundle when
|
|
14
|
+
# routed through `Merger.merge`.
|
|
15
|
+
#
|
|
16
|
+
# Plugin authors should not depend on the element shape — the
|
|
17
|
+
# bundle is the public contract; the element list is the
|
|
18
|
+
# implementation surface the merge policy operates over.
|
|
19
|
+
ELEMENT_VALID_EDGES = %i[normal truthy falsey post_return exceptional].freeze
|
|
20
|
+
ELEMENT_VALID_KINDS = %i[
|
|
21
|
+
return_type
|
|
22
|
+
truthy_fact
|
|
23
|
+
falsey_fact
|
|
24
|
+
post_return_fact
|
|
25
|
+
mutation
|
|
26
|
+
invalidation
|
|
27
|
+
exception
|
|
28
|
+
role
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
Element = Data.define(:target, :edge, :kind, :payload, :provenance) do
|
|
32
|
+
def initialize(target:, edge:, kind:, payload:, provenance:)
|
|
33
|
+
unless ELEMENT_VALID_EDGES.include?(edge)
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"FlowContribution::Element edge must be one of " \
|
|
36
|
+
"#{ELEMENT_VALID_EDGES.inspect}, got #{edge.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless ELEMENT_VALID_KINDS.include?(kind)
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"FlowContribution::Element kind must be one of " \
|
|
42
|
+
"#{ELEMENT_VALID_KINDS.inspect}, got #{kind.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge_key
|
|
49
|
+
[target, edge, kind]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Canonical slot payload for the four edge-aware fact slots
|
|
6
|
+
# (`truthy_facts`, `falsey_facts`, `post_return_facts`, plus
|
|
7
|
+
# the equivalent under `mutations` / `invalidations` /
|
|
8
|
+
# `role_conformance` once those carriers grow Fact-shaped
|
|
9
|
+
# variants).
|
|
10
|
+
#
|
|
11
|
+
# ADR-7 § "Slice 4-A" pins this object as the **single
|
|
12
|
+
# canonical translation target** for the four parallel
|
|
13
|
+
# contribution carriers the engine has carried so far:
|
|
14
|
+
#
|
|
15
|
+
# 1. Built-in narrowing rules' direct fact emission
|
|
16
|
+
# (Inference::Narrowing#predicate_scopes).
|
|
17
|
+
# 2. RBS::Extended `predicate-if-*` directives
|
|
18
|
+
# (`Rigor::RbsExtended::PredicateEffect`).
|
|
19
|
+
# 3. RBS::Extended `assert*` directives
|
|
20
|
+
# (`Rigor::RbsExtended::AssertEffect`).
|
|
21
|
+
# 4. Future plugin contributions (slice 5 emission protocol).
|
|
22
|
+
#
|
|
23
|
+
# Each of those four carriers translates to / from Fact at
|
|
24
|
+
# its boundary; downstream of {Rigor::FlowContribution#to_element_list}
|
|
25
|
+
# and {Rigor::FlowContribution::Merger.merge}, every slot
|
|
26
|
+
# payload is a Fact (or a value that the merger compares by
|
|
27
|
+
# equality and never inspects). The typed `RbsExtended::*Effect`
|
|
28
|
+
# carriers stay internal to the parser side — they hold the
|
|
29
|
+
# source-text shape, but lose their identity at the
|
|
30
|
+
# `read_flow_contribution` boundary.
|
|
31
|
+
#
|
|
32
|
+
# ## Field set
|
|
33
|
+
#
|
|
34
|
+
# - `target_kind`: `:parameter` (call-site argument) or
|
|
35
|
+
# `:self` (receiver). Future slices may extend the set
|
|
36
|
+
# (`:local`, `:ivar`, `:result`); the merger is agnostic
|
|
37
|
+
# to the concrete kinds and only requires equality.
|
|
38
|
+
# - `target_name`: a `Symbol`. For `:parameter` it's the
|
|
39
|
+
# declared parameter name. For `:self` it is the literal
|
|
40
|
+
# `:self` symbol so the field stays non-nil and the merge
|
|
41
|
+
# key is well-defined.
|
|
42
|
+
# - `type`: a `Rigor::Type::*` (Nominal, Refined,
|
|
43
|
+
# IntegerRange, Difference, …) the fact narrows the
|
|
44
|
+
# target toward (when `negative` is false) or away from
|
|
45
|
+
# (when `negative` is true).
|
|
46
|
+
# - `negative`: `true` for the `~T` negation form
|
|
47
|
+
# (`predicate-if-true x is ~Integer`), `false` for the
|
|
48
|
+
# plain positive form. Mirrors the `negative` field on
|
|
49
|
+
# `PredicateEffect` / `AssertEffect`.
|
|
50
|
+
#
|
|
51
|
+
# The `target` accessor returns `:self` for self-targeted
|
|
52
|
+
# facts and `[:parameter, name]` otherwise — that's the
|
|
53
|
+
# value {Element#target} keys on, so two facts that narrow
|
|
54
|
+
# the same parameter from different contribution sources
|
|
55
|
+
# land in the same merge bucket.
|
|
56
|
+
FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
|
|
57
|
+
|
|
58
|
+
Fact = Data.define(:target_kind, :target_name, :type, :negative) do
|
|
59
|
+
def initialize(target_kind:, target_name:, type:, negative: false)
|
|
60
|
+
unless FACT_VALID_TARGET_KINDS.include?(target_kind)
|
|
61
|
+
raise ArgumentError,
|
|
62
|
+
"FlowContribution::Fact target_kind must be one of " \
|
|
63
|
+
"#{FACT_VALID_TARGET_KINDS.inspect}, got #{target_kind.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless target_name.is_a?(Symbol)
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"FlowContribution::Fact target_name must be a Symbol, got #{target_name.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Composite target identifier the merger keys on. `:self`
|
|
75
|
+
# for self-targeted facts; otherwise `[:parameter, name]`
|
|
76
|
+
# so two contributions that narrow the same parameter
|
|
77
|
+
# (regardless of source family) land in the same merge
|
|
78
|
+
# bucket.
|
|
79
|
+
def target
|
|
80
|
+
target_kind == :self ? :self : [target_kind, target_name]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def negative?
|
|
84
|
+
negative == true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Result of folding any number of {FlowContribution} bundles
|
|
6
|
+
# through {Merger.merge}. Surfaces the merged content slot-by-
|
|
7
|
+
# slot, the ordered list of contributing provenances, and the
|
|
8
|
+
# {Conflict} list collected along the way.
|
|
9
|
+
#
|
|
10
|
+
# The merge result is a sibling shape of {FlowContribution} —
|
|
11
|
+
# the analyzer reads from it to drive narrowing / dispatch /
|
|
12
|
+
# diagnostics, and the formatter reads from it to surface
|
|
13
|
+
# plugin / RBS::Extended provenance. The shape is derived per
|
|
14
|
+
# ADR-2 § "Plugin Contribution Merging"; see
|
|
15
|
+
# [`docs/internal-spec/flow-contribution-merger.md`](../../../docs/internal-spec/flow-contribution-merger.md)
|
|
16
|
+
# for the slice-3 normative description.
|
|
17
|
+
class MergeResult
|
|
18
|
+
attr_reader :return_type, :truthy_facts, :falsey_facts, :post_return_facts,
|
|
19
|
+
:mutations, :invalidations, :exceptional, :role_conformance,
|
|
20
|
+
:provenances, :conflicts
|
|
21
|
+
|
|
22
|
+
# rubocop:disable Metrics/ParameterLists
|
|
23
|
+
def initialize(return_type: nil, truthy_facts: [], falsey_facts: [],
|
|
24
|
+
post_return_facts: [], mutations: [], invalidations: [],
|
|
25
|
+
exceptional: nil, role_conformance: [],
|
|
26
|
+
provenances: [], conflicts: [])
|
|
27
|
+
# rubocop:enable Metrics/ParameterLists
|
|
28
|
+
@return_type = return_type
|
|
29
|
+
@truthy_facts = truthy_facts.dup.freeze
|
|
30
|
+
@falsey_facts = falsey_facts.dup.freeze
|
|
31
|
+
@post_return_facts = post_return_facts.dup.freeze
|
|
32
|
+
@mutations = mutations.dup.freeze
|
|
33
|
+
@invalidations = invalidations.dup.freeze
|
|
34
|
+
@exceptional = exceptional
|
|
35
|
+
@role_conformance = role_conformance.dup.freeze
|
|
36
|
+
@provenances = provenances.dup.freeze
|
|
37
|
+
@conflicts = conflicts.dup.freeze
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def conflict?
|
|
42
|
+
!@conflicts.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty? # rubocop:disable Metrics/CyclomaticComplexity
|
|
46
|
+
@return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
|
|
47
|
+
@post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
|
|
48
|
+
@exceptional.nil? && @role_conformance.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
{
|
|
53
|
+
"return_type" => return_type,
|
|
54
|
+
"truthy_facts" => truthy_facts,
|
|
55
|
+
"falsey_facts" => falsey_facts,
|
|
56
|
+
"post_return_facts" => post_return_facts,
|
|
57
|
+
"mutations" => mutations,
|
|
58
|
+
"invalidations" => invalidations,
|
|
59
|
+
"exceptional" => exceptional,
|
|
60
|
+
"role_conformance" => role_conformance,
|
|
61
|
+
"provenances" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p },
|
|
62
|
+
"conflicts" => conflicts.map(&:to_h)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|