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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -22
  3. data/data/builtins/ruby_core/encoding.yml +210 -0
  4. data/data/builtins/ruby_core/exception.yml +641 -0
  5. data/data/builtins/ruby_core/numeric.yml +3 -2
  6. data/data/builtins/ruby_core/proc.yml +731 -0
  7. data/data/builtins/ruby_core/random.yml +166 -0
  8. data/data/builtins/ruby_core/re.yml +689 -0
  9. data/data/builtins/ruby_core/struct.yml +449 -0
  10. data/lib/rigor/analysis/check_rules.rb +228 -40
  11. data/lib/rigor/analysis/diagnostic.rb +15 -1
  12. data/lib/rigor/analysis/runner.rb +199 -4
  13. data/lib/rigor/builtins/imported_refinements.rb +6 -1
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
  16. data/lib/rigor/cache/rbs_constant_table.rb +15 -51
  17. data/lib/rigor/cache/rbs_descriptor.rb +55 -0
  18. data/lib/rigor/cache/rbs_environment.rb +52 -0
  19. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
  20. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  21. data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
  22. data/lib/rigor/cache/store.rb +81 -15
  23. data/lib/rigor/cli.rb +45 -7
  24. data/lib/rigor/configuration/severity_profile.rb +109 -0
  25. data/lib/rigor/configuration.rb +110 -6
  26. data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
  27. data/lib/rigor/environment/rbs_loader.rb +220 -32
  28. data/lib/rigor/environment.rb +11 -2
  29. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  30. data/lib/rigor/flow_contribution/element.rb +53 -0
  31. data/lib/rigor/flow_contribution/fact.rb +88 -0
  32. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  33. data/lib/rigor/flow_contribution/merger.rb +275 -0
  34. data/lib/rigor/flow_contribution.rb +179 -0
  35. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  36. data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
  37. data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
  38. data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
  39. data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
  40. data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
  41. data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
  42. data/lib/rigor/inference/expression_typer.rb +110 -6
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
  44. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
  45. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  46. data/lib/rigor/inference/method_dispatcher.rb +2 -0
  47. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  48. data/lib/rigor/inference/narrowing.rb +134 -144
  49. data/lib/rigor/inference/scope_indexer.rb +75 -1
  50. data/lib/rigor/inference/statement_evaluator.rb +380 -40
  51. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  52. data/lib/rigor/plugin/base.rb +241 -0
  53. data/lib/rigor/plugin/io_boundary.rb +102 -0
  54. data/lib/rigor/plugin/load_error.rb +23 -0
  55. data/lib/rigor/plugin/loader.rb +191 -0
  56. data/lib/rigor/plugin/manifest.rb +134 -0
  57. data/lib/rigor/plugin/registry.rb +50 -0
  58. data/lib/rigor/plugin/services.rb +65 -0
  59. data/lib/rigor/plugin/trust_policy.rb +99 -0
  60. data/lib/rigor/plugin.rb +61 -0
  61. data/lib/rigor/rbs_extended.rb +103 -0
  62. data/lib/rigor/reflection.rb +2 -2
  63. data/lib/rigor/type/combinator.rb +72 -0
  64. data/lib/rigor/type/refined.rb +50 -2
  65. data/lib/rigor/version.rb +1 -1
  66. data/lib/rigor.rb +13 -0
  67. data/sig/rigor/environment.rbs +7 -1
  68. data/sig/rigor/inference.rbs +1 -0
  69. data/sig/rigor/rbs_extended.rbs +2 -0
  70. data/sig/rigor/scope.rbs +1 -0
  71. data/sig/rigor/type.rbs +7 -0
  72. data/sig/rigor.rbs +3 -1
  73. metadata +38 -1
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ # Read-side query API over the plugins loaded for a single
6
+ # `Analysis::Runner.run`. Constructed by
7
+ # {Rigor::Plugin::Loader.load} and exposed downstream so the
8
+ # contribution merger (slice 3) and diagnostic provenance
9
+ # (slice 5) can iterate over loaded plugin instances in
10
+ # deterministic order.
11
+ #
12
+ # The registry is read-only after construction; ordering is
13
+ # the order in which {Rigor::Plugin::Loader} resolved
14
+ # configuration entries, which is project-config order with
15
+ # plugin-id alphabetical as the tie-breaker.
16
+ class Registry
17
+ attr_reader :plugins, :load_errors
18
+
19
+ # @param plugins [Array<Rigor::Plugin::Base>] instantiated
20
+ # plugin instances in deterministic order.
21
+ # @param load_errors [Array<Rigor::Plugin::LoadError>] failures
22
+ # surfaced during loading. Each error is also turned into a
23
+ # diagnostic by the runner.
24
+ def initialize(plugins: [], load_errors: [])
25
+ @plugins = plugins.dup.freeze
26
+ @load_errors = load_errors.dup.freeze
27
+ freeze
28
+ end
29
+
30
+ def find(id)
31
+ id_s = id.to_s
32
+ plugins.find { |plugin| plugin.manifest.id == id_s }
33
+ end
34
+
35
+ def ids
36
+ plugins.map { |plugin| plugin.manifest.id }
37
+ end
38
+
39
+ def empty?
40
+ plugins.empty?
41
+ end
42
+
43
+ def any_load_errors?
44
+ !load_errors.empty?
45
+ end
46
+
47
+ EMPTY = new.freeze
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ # Dependency-injection container handed to every plugin's
6
+ # {Rigor::Plugin::Base#init} method. Plugins read from the
7
+ # container; they MUST NOT mutate it. The container is
8
+ # constructed once per `Analysis::Runner.run` and destroyed
9
+ # at the end of the run.
10
+ #
11
+ # ADR-2 § "Registration, Configuration, and Caching" reserves
12
+ # this surface for "constructor injection for analyzer
13
+ # services such as reflection providers, type factories,
14
+ # loggers, and configuration readers". Slice 1 wires four
15
+ # of those:
16
+ #
17
+ # - `reflection`: the {Rigor::Reflection} read-side facade.
18
+ # - `type`: the {Rigor::Type::Combinator} factory module.
19
+ # - `configuration`: the project's {Rigor::Configuration}.
20
+ # - `cache_store`: the {Rigor::Cache::Store} the run is using
21
+ # (or `nil` when caching is disabled). Slice 6 wires
22
+ # plugin-side cache producers through this entry.
23
+ #
24
+ # Loggers are not yet a public surface in the core analyzer;
25
+ # they will be added when the diagnostics formatter grows a
26
+ # progress channel.
27
+ #
28
+ # Slice 2 (Plugin trust / I/O policy) extends the container
29
+ # with `trust_policy` and a per-plugin `io_boundary_for(plugin_id)`
30
+ # factory. Plugins should reach for the boundary rather than
31
+ # raw `File.read` so reads stay within the trusted scope and
32
+ # feed cache invalidation; ADR-2 § "Plugin Trust and I/O
33
+ # Policy" documents the trust model the boundary enforces.
34
+ class Services
35
+ attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy
36
+
37
+ def initialize(reflection:, type:, configuration:, cache_store: nil, trust_policy: nil)
38
+ @reflection = reflection
39
+ @type = type
40
+ @configuration = configuration
41
+ @cache_store = cache_store
42
+ @trust_policy = trust_policy || default_trust_policy
43
+ freeze
44
+ end
45
+
46
+ # Returns a fresh {IoBoundary} bound to `plugin_id` and the
47
+ # current `trust_policy`. The boundary accumulates per-plugin
48
+ # cache descriptor entries; the loader / contribution merger
49
+ # constructs one boundary per plugin per run.
50
+ def io_boundary_for(plugin_id)
51
+ IoBoundary.new(policy: @trust_policy, plugin_id: plugin_id)
52
+ end
53
+
54
+ private
55
+
56
+ def default_trust_policy
57
+ TrustPolicy.new(
58
+ trusted_gems: [],
59
+ allowed_read_roots: [Dir.pwd],
60
+ network_policy: :disabled
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ # Declarative trust / I/O policy for the active plugin set.
6
+ # Pinned by [ADR-2 § "Plugin Trust and I/O Policy"](../../../docs/adr/2-extension-api.md):
7
+ # plugins are *trusted Ruby gems selected by the user, their
8
+ # Gemfile, or project configuration*; this class is the
9
+ # programmatic surface that documents that trust and lets the
10
+ # analyzer enforce read scope + network disablement at the
11
+ # documented edges.
12
+ #
13
+ # The policy is **not a sandbox.** A plugin that uses raw
14
+ # `File.read` or `Net::HTTP` bypasses the policy — ADR-2
15
+ # explicitly chooses documentation over forced isolation. The
16
+ # contract is: when plugins go through {Rigor::Plugin::IoBoundary}
17
+ # (the analyzer-side helper service slice 2 introduces), the
18
+ # boundary checks against this policy and feeds compliant reads
19
+ # into the cache descriptor for invalidation. Slices 3-6 wire
20
+ # plugin contributions through the boundary so the policy is
21
+ # the actual mechanism, not just paperwork.
22
+ #
23
+ # ## Fields
24
+ #
25
+ # - `trusted_gems`: gem names the user has authorised. Derived
26
+ # from the `plugins:` section of `.rigor.yml` plus any gems
27
+ # they reach transitively. Used today for documentation and
28
+ # future trust diagnostics.
29
+ # - `allowed_read_roots`: absolute paths plugin code may read
30
+ # from through the {IoBoundary}. The default set covers the
31
+ # project root, the project's `signature_paths`, the active
32
+ # `Gemfile.lock`, and each trusted gem's
33
+ # `Gem::Specification#full_gem_path`. The user extends this
34
+ # with `.rigor.yml`'s `plugins_io.allowed_paths:`.
35
+ # - `network_policy`: `:disabled` in slice 2; the only value
36
+ # accepted today. Plugin {IoBoundary#open_url} always raises
37
+ # while the policy is `:disabled`.
38
+ class TrustPolicy
39
+ VALID_NETWORK_POLICIES = %i[disabled].freeze
40
+
41
+ attr_reader :trusted_gems, :allowed_read_roots, :network_policy
42
+
43
+ def initialize(trusted_gems: [], allowed_read_roots: [], network_policy: :disabled)
44
+ validate_network_policy!(network_policy)
45
+
46
+ @trusted_gems = trusted_gems.map { |g| g.to_s.dup.freeze }.uniq.sort.freeze
47
+ @allowed_read_roots = allowed_read_roots
48
+ .map { |path| File.expand_path(path).freeze }
49
+ .uniq
50
+ .sort
51
+ .freeze
52
+ @network_policy = network_policy
53
+ freeze
54
+ end
55
+
56
+ # @param path [String]
57
+ # @return [Boolean] true when the absolute path falls inside
58
+ # any allowed read root. Symlinks are resolved through
59
+ # `File.expand_path` only (no `realpath`); plugins with
60
+ # adversarial intent are out of scope per ADR-2.
61
+ def allow_read?(path)
62
+ absolute = File.expand_path(path.to_s)
63
+ @allowed_read_roots.any? { |root| inside?(absolute, root) }
64
+ end
65
+
66
+ def network_allowed?
67
+ @network_policy != :disabled
68
+ end
69
+
70
+ def gem_trusted?(name)
71
+ @trusted_gems.include?(name.to_s)
72
+ end
73
+
74
+ def to_h
75
+ {
76
+ "trusted_gems" => trusted_gems,
77
+ "allowed_read_roots" => allowed_read_roots,
78
+ "network_policy" => network_policy.to_s
79
+ }
80
+ end
81
+
82
+ private
83
+
84
+ def validate_network_policy!(policy)
85
+ return if VALID_NETWORK_POLICIES.include?(policy)
86
+
87
+ raise ArgumentError,
88
+ "TrustPolicy network_policy must be one of #{VALID_NETWORK_POLICIES.inspect}, got #{policy.inspect}"
89
+ end
90
+
91
+ def inside?(absolute, root)
92
+ return true if absolute == root
93
+
94
+ prefix = "#{root}#{File::SEPARATOR}"
95
+ absolute.start_with?(prefix)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "plugin/manifest"
4
+ require_relative "plugin/access_denied_error"
5
+ require_relative "plugin/trust_policy"
6
+ require_relative "plugin/io_boundary"
7
+ require_relative "plugin/services"
8
+ require_relative "plugin/base"
9
+ require_relative "plugin/registry"
10
+ require_relative "plugin/load_error"
11
+
12
+ module Rigor
13
+ module Plugin
14
+ @registered = {}
15
+ @mutex = Mutex.new
16
+
17
+ class << self
18
+ def register(plugin_class)
19
+ unless plugin_class.is_a?(Class) && plugin_class < Base
20
+ raise ArgumentError,
21
+ "Rigor::Plugin.register expects a subclass of Rigor::Plugin::Base, got #{plugin_class.inspect}"
22
+ end
23
+
24
+ manifest = plugin_class.manifest # rigor:disable undefined-method
25
+ @mutex.synchronize do
26
+ existing = @registered[manifest.id]
27
+ if existing && existing != plugin_class
28
+ raise LoadError.new(
29
+ "plugin id #{manifest.id.inspect} already registered to #{existing}, " \
30
+ "cannot re-register to #{plugin_class}",
31
+ plugin_ref: manifest.id
32
+ )
33
+ end
34
+
35
+ @registered[manifest.id] = plugin_class
36
+ end
37
+ plugin_class
38
+ end
39
+
40
+ def registered_for(id)
41
+ @mutex.synchronize { @registered[id.to_s] }
42
+ end
43
+
44
+ def registered
45
+ @mutex.synchronize { @registered.dup.freeze }
46
+ end
47
+
48
+ def unregister!(id = nil)
49
+ @mutex.synchronize do
50
+ if id.nil?
51
+ @registered.clear
52
+ else
53
+ @registered.delete(id.to_s)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ require_relative "plugin/loader"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "type"
4
4
  require_relative "builtins/imported_refinements"
5
+ require_relative "flow_contribution"
5
6
 
6
7
  module Rigor
7
8
  # Slice 7 phase 15 — first-preview reader for the
@@ -63,6 +64,23 @@ module Rigor
63
64
  def falsey_only? = edge == :falsey_only
64
65
  def negative? = negative == true
65
66
  def refinement? = !refinement_type.nil?
67
+
68
+ # ADR-7 § "Slice 4-A" canonical translation. Lifts the
69
+ # parser-side carrier into a `Rigor::FlowContribution::Fact`
70
+ # that the merger and plugin contribution stream consume
71
+ # uniformly. `class_name` lifts to `Nominal[<class>]`;
72
+ # `refinement_type` is already a `Rigor::Type` and passes
73
+ # through. The `edge` field doesn't survive the conversion —
74
+ # the slot it lands in (truthy_facts / falsey_facts / ...)
75
+ # encodes that.
76
+ def to_fact
77
+ FlowContribution::Fact.new(
78
+ target_kind: target_kind,
79
+ target_name: target_name,
80
+ type: refinement_type || Rigor::Type::Combinator.nominal_of(class_name),
81
+ negative: negative == true
82
+ )
83
+ end
66
84
  end
67
85
 
68
86
  # Returned for `assert` / `assert-if-true` /
@@ -86,6 +104,21 @@ module Rigor
86
104
  def if_falsey_return? = condition == :if_falsey_return
87
105
  def negative? = negative == true
88
106
  def refinement? = !refinement_type.nil?
107
+
108
+ # ADR-7 § "Slice 4-A" canonical translation. Same shape as
109
+ # `PredicateEffect#to_fact`; the `condition` field
110
+ # (`:always` / `:if_truthy_return` / `:if_falsey_return`)
111
+ # routes which slot the resulting fact lands in at the
112
+ # `read_flow_contribution` boundary, but does not surface
113
+ # on the Fact itself.
114
+ def to_fact
115
+ FlowContribution::Fact.new(
116
+ target_kind: target_kind,
117
+ target_name: target_name,
118
+ type: refinement_type || Rigor::Type::Combinator.nominal_of(class_name),
119
+ negative: negative == true
120
+ )
121
+ end
89
122
  end
90
123
 
91
124
  module_function
@@ -410,5 +443,75 @@ module Rigor
410
443
 
411
444
  ParamOverride.new(param_name: match[:param].to_sym, type: type)
412
445
  end
446
+
447
+ # The shared {Rigor::FlowContribution::Provenance} for every
448
+ # bundle this module produces. `source_family: :rbs_extended`
449
+ # so consumers (today the documentation surface; v0.1.0 the
450
+ # plugin contribution merger) can attribute facts back to the
451
+ # RBS::Extended layer.
452
+ RBS_EXTENDED_PROVENANCE = FlowContribution::Provenance.new(
453
+ source_family: :rbs_extended,
454
+ plugin_id: nil,
455
+ node: nil,
456
+ descriptor: nil
457
+ ).freeze
458
+
459
+ # Rolls up every recognised RBS::Extended directive on
460
+ # `method_def` into a single {Rigor::FlowContribution} with
461
+ # the canonical {Rigor::FlowContribution::Fact} payload (see
462
+ # ADR-7 § "Slice 4-A"):
463
+ #
464
+ # - `predicate-if-true` → `truthy_facts`
465
+ # - `predicate-if-false` → `falsey_facts`
466
+ # - `assert` → `post_return_facts`
467
+ # - `assert-if-true` → `truthy_facts`
468
+ # - `assert-if-false` → `falsey_facts`
469
+ # - `return:` override → `return_type` (`Rigor::Type`)
470
+ #
471
+ # Param overrides are intentionally NOT included — they refine
472
+ # the call's signature contract rather than its flow facts and
473
+ # do not fit ADR-2 § "Flow Contribution Bundle" slot semantics.
474
+ # Callers that care about parameter contracts keep using
475
+ # {.read_param_type_overrides} / {.param_type_override_map}.
476
+ #
477
+ # Returns `nil` when the method carries no recognised
478
+ # contribution directives (callers can skip the merge step
479
+ # without iterating an empty bundle).
480
+ def read_flow_contribution(method_def)
481
+ return nil if method_def.nil?
482
+
483
+ predicate_effects = read_predicate_effects(method_def)
484
+ assert_effects = read_assert_effects(method_def)
485
+ return_override = read_return_type_override(method_def)
486
+ return nil if predicate_effects.empty? && assert_effects.empty? && return_override.nil?
487
+
488
+ build_flow_contribution(predicate_effects, assert_effects, return_override)
489
+ end
490
+
491
+ def build_flow_contribution(predicate_effects, assert_effects, return_override) # rubocop:disable Metrics/CyclomaticComplexity
492
+ truthy = predicate_effects.select(&:truthy_only?).map(&:to_fact)
493
+ falsey = predicate_effects.select(&:falsey_only?).map(&:to_fact)
494
+ post_return = []
495
+
496
+ assert_effects.each do |effect|
497
+ case effect.condition
498
+ when :if_truthy_return then truthy << effect.to_fact
499
+ when :if_falsey_return then falsey << effect.to_fact
500
+ else post_return << effect.to_fact
501
+ end
502
+ end
503
+
504
+ FlowContribution.new(
505
+ return_type: return_override,
506
+ truthy_facts: nilable_slot(truthy),
507
+ falsey_facts: nilable_slot(falsey),
508
+ post_return_facts: nilable_slot(post_return),
509
+ provenance: RBS_EXTENDED_PROVENANCE
510
+ )
511
+ end
512
+
513
+ def nilable_slot(facts)
514
+ facts.empty? ? nil : facts
515
+ end
413
516
  end
414
517
  end
@@ -147,7 +147,7 @@ module Rigor
147
147
  return nil if loader.nil?
148
148
 
149
149
  loader.instance_definition(class_name.to_s)
150
- rescue StandardError
150
+ rescue ::RBS::BaseError
151
151
  nil
152
152
  end
153
153
 
@@ -157,7 +157,7 @@ module Rigor
157
157
  return nil if loader.nil?
158
158
 
159
159
  loader.singleton_definition(class_name.to_s)
160
- rescue StandardError
160
+ rescue ::RBS::BaseError
161
161
  nil
162
162
  end
163
163
 
@@ -149,14 +149,40 @@ module Rigor
149
149
  Refined.new(nominal_of("String"), :lowercase)
150
150
  end
151
151
 
152
+ # Complement of `lowercase-string`: a `String` with at least
153
+ # one non-lowercase character (i.e. `v != v.downcase`).
154
+ # Registered as the paired complement of
155
+ # `:lowercase` in {Refined::COMPLEMENT_PAIRS} so
156
+ # `~lowercase-string` narrows to this carrier instead of
157
+ # falling back to `Difference[String, lowercase-string]`.
158
+ def non_lowercase_string
159
+ Refined.new(nominal_of("String"), :not_lowercase)
160
+ end
161
+
152
162
  def uppercase_string
153
163
  Refined.new(nominal_of("String"), :uppercase)
154
164
  end
155
165
 
166
+ # Complement of `uppercase-string`: a `String` with at least
167
+ # one non-uppercase character. Paired with `:uppercase` in
168
+ # {Refined::COMPLEMENT_PAIRS}.
169
+ def non_uppercase_string
170
+ Refined.new(nominal_of("String"), :not_uppercase)
171
+ end
172
+
156
173
  def numeric_string
157
174
  Refined.new(nominal_of("String"), :numeric)
158
175
  end
159
176
 
177
+ # Complement of `numeric-string`: a `String` that is not
178
+ # accepted by Rigor's Ruby numeric-string predicate
179
+ # (contains at least one non-digit, has a malformed numeric
180
+ # form, etc.). Paired with `:numeric` in
181
+ # {Refined::COMPLEMENT_PAIRS}.
182
+ def non_numeric_string
183
+ Refined.new(nominal_of("String"), :not_numeric)
184
+ end
185
+
160
186
  def decimal_int_string
161
187
  Refined.new(nominal_of("String"), :decimal_int)
162
188
  end
@@ -169,6 +195,52 @@ module Rigor
169
195
  Refined.new(nominal_of("String"), :hex_int)
170
196
  end
171
197
 
198
+ # `literal-string` — a `String` that is statically known to
199
+ # come from a source-code literal (or a composition of
200
+ # literals). v0.0.9 tracks this flow through interpolation
201
+ # `"#{...}"`, leaving propagation through `+` / `<<` to a
202
+ # later slice. Every `Constant<String>` is implicitly
203
+ # literal-string-compatible; the carrier exists for cases
204
+ # where the concrete value is unknown but literal-ness has
205
+ # been established (an RBS::Extended `return: literal-string`
206
+ # annotation, or interpolation over literal-bearing parts).
207
+ def literal_string
208
+ Refined.new(nominal_of("String"), :literal_string)
209
+ end
210
+
211
+ # `non-empty-literal-string` = `non-empty-string ∩ literal-string`.
212
+ # Composes the point-removal half (`Difference[String, ""]`)
213
+ # with the predicate-subset half. Both members erase to
214
+ # `String`.
215
+ def non_empty_literal_string
216
+ intersection(non_empty_string, literal_string)
217
+ end
218
+
219
+ # Recognises the carriers that participate in literal-string
220
+ # flow tracking: any `Constant<String>` (constants are literal
221
+ # by construction), the `literal-string` Refined carrier, an
222
+ # `Intersection` containing `literal-string`, or a `Union`
223
+ # whose every member qualifies. Used by
224
+ # `ExpressionTyper#type_of_interpolated_string` and the
225
+ # `LiteralStringFolding` dispatcher tier so propagation
226
+ # through interpolation and `+`/`*` composition stays
227
+ # consistent.
228
+ def literal_string_compatible?(type)
229
+ case type
230
+ when Constant then type.value.is_a?(String)
231
+ when Refined then literal_string_carrier?(type)
232
+ when Intersection then type.members.any? { |m| literal_string_compatible?(m) }
233
+ when Union then type.members.all? { |m| literal_string_compatible?(m) }
234
+ else false
235
+ end
236
+ end
237
+
238
+ def literal_string_carrier?(refined)
239
+ refined.predicate_id == :literal_string &&
240
+ refined.base.is_a?(Nominal) &&
241
+ refined.base.class_name == "String"
242
+ end
243
+
172
244
  # Normalised intersection. Flattens nested Intersections,
173
245
  # drops `Top` members, collapses to `Bot` if any member is
174
246
  # `Bot`, deduplicates structurally-equal members, sorts the
@@ -141,11 +141,24 @@ module Rigor
141
141
 
142
142
  PREDICATES = {
143
143
  lowercase: ->(v) { v.is_a?(String) && v == v.downcase },
144
+ not_lowercase: ->(v) { v.is_a?(String) && v != v.downcase },
144
145
  uppercase: ->(v) { v.is_a?(String) && v == v.upcase },
146
+ not_uppercase: ->(v) { v.is_a?(String) && v != v.upcase },
145
147
  numeric: ->(v) { v.is_a?(String) && NUMERIC_STRING_PATTERN.match?(v) },
148
+ not_numeric: ->(v) { v.is_a?(String) && !NUMERIC_STRING_PATTERN.match?(v) },
146
149
  decimal_int: ->(v) { v.is_a?(String) && DECIMAL_INT_STRING_PATTERN.match?(v) },
147
150
  octal_int: ->(v) { v.is_a?(String) && OCTAL_INT_STRING_PATTERN.match?(v) },
148
- hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) }
151
+ hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) },
152
+ # `literal-string` is a flow-tracked predicate, not a value-
153
+ # level predicate: a String is literal-string when it is
154
+ # known to come from a source-code literal (or composition
155
+ # of literals). Every concrete `Constant<String>` is
156
+ # already literal by construction, so the inspection
157
+ # recogniser returns true for any String — the property is
158
+ # really tracked in the flow analysis (interpolation,
159
+ # concatenation, RBS::Extended `return: literal-string`)
160
+ # rather than recovered by inspecting an arbitrary string.
161
+ literal_string: ->(v) { v.is_a?(String) }
149
162
  }.freeze
150
163
 
151
164
  # Maps `[base_class_name, predicate_id]` pairs to their
@@ -154,14 +167,49 @@ module Rigor
154
167
  # to the operator form.
155
168
  CANONICAL_NAMES = {
156
169
  ["String", :lowercase] => "lowercase-string",
170
+ ["String", :not_lowercase] => "non-lowercase-string",
157
171
  ["String", :uppercase] => "uppercase-string",
172
+ ["String", :not_uppercase] => "non-uppercase-string",
158
173
  ["String", :numeric] => "numeric-string",
174
+ ["String", :not_numeric] => "non-numeric-string",
159
175
  ["String", :decimal_int] => "decimal-int-string",
160
176
  ["String", :octal_int] => "octal-int-string",
161
- ["String", :hex_int] => "hex-int-string"
177
+ ["String", :hex_int] => "hex-int-string",
178
+ ["String", :literal_string] => "literal-string"
162
179
  }.freeze
163
180
  private_constant :CANONICAL_NAMES
164
181
 
182
+ # Bidirectional `predicate_id ↔ complement_predicate_id`
183
+ # registry. `~Refined[base, p]` narrows to
184
+ # `Refined[base, COMPLEMENT_PAIRS[p]]` when the part is the
185
+ # refinement's base — the precise carrier the spec promises
186
+ # under the `~T` operator. Predicates without a registered
187
+ # complement fall back to the imprecise but sound
188
+ # `Difference[part, refined]` carrier from the existing
189
+ # narrowing rule.
190
+ #
191
+ # Adding a new pair here is an additive change: register the
192
+ # complement predicate in {PREDICATES}, give it a kebab-case
193
+ # canonical name in {CANONICAL_NAMES}, and add the bidirectional
194
+ # entry below. No call site needs to know about the new pair —
195
+ # `complement_refined` consults this map and routes through
196
+ # the registered complement automatically.
197
+ COMPLEMENT_PAIRS = {
198
+ lowercase: :not_lowercase,
199
+ not_lowercase: :lowercase,
200
+ uppercase: :not_uppercase,
201
+ not_uppercase: :uppercase,
202
+ numeric: :not_numeric,
203
+ not_numeric: :numeric
204
+ }.freeze
205
+ private_constant :COMPLEMENT_PAIRS
206
+
207
+ # @return [Symbol, nil] the registered complement predicate
208
+ # id, or nil when no pair is registered for this predicate.
209
+ def complement_predicate_id
210
+ COMPLEMENT_PAIRS[predicate_id]
211
+ end
212
+
165
213
  private
166
214
 
167
215
  def canonical_name
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.0.8"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/rigor.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "rigor/version"
4
4
  require_relative "rigor/configuration"
5
+ require_relative "rigor/configuration/severity_profile"
5
6
  require_relative "rigor/trinary"
6
7
  require_relative "rigor/type"
7
8
  require_relative "rigor/ast"
@@ -21,7 +22,19 @@ require_relative "rigor/scope"
21
22
  require_relative "rigor/reflection"
22
23
  require_relative "rigor/cache/descriptor"
23
24
  require_relative "rigor/cache/store"
25
+ require_relative "rigor/cache/rbs_descriptor"
24
26
  require_relative "rigor/cache/rbs_constant_table"
27
+ require_relative "rigor/cache/rbs_known_class_names"
28
+ require_relative "rigor/cache/rbs_class_ancestor_table"
29
+ require_relative "rigor/cache/rbs_class_type_param_names"
30
+ require_relative "rigor/cache/rbs_environment"
31
+ require_relative "rigor/cache/rbs_instance_definitions"
32
+ require_relative "rigor/flow_contribution"
33
+ require_relative "rigor/flow_contribution/fact"
34
+ require_relative "rigor/flow_contribution/conflict"
35
+ require_relative "rigor/flow_contribution/merge_result"
36
+ require_relative "rigor/flow_contribution/merger"
37
+ require_relative "rigor/plugin"
25
38
  require_relative "rigor/source"
26
39
  require_relative "rigor/inference/scope_indexer"
27
40
  require_relative "rigor/inference/coverage_scanner"
@@ -32,19 +32,25 @@ module Rigor
32
32
  class RbsLoader
33
33
  attr_reader libraries: Array[String]
34
34
  attr_reader signature_paths: Array[String | _ToPath]
35
+ attr_reader cache_store: untyped?
35
36
 
36
37
  def self.default: () -> RbsLoader
37
38
  def self.reset_default!: () -> void
39
+ def self.build_env_for: (libraries: Array[String], signature_paths: Array[String | _ToPath]) -> untyped
38
40
 
39
- def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath]) -> void
41
+ def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath], ?cache_store: untyped?) -> void
40
42
  def class_known?: (String | Symbol name) -> bool
41
43
  def instance_definition: (String | Symbol class_name) -> untyped?
42
44
  def instance_method: (class_name: String | Symbol, method_name: String | Symbol) -> untyped?
45
+ def uncached_instance_definition: (String | Symbol class_name) -> untyped?
43
46
  def singleton_definition: (String | Symbol class_name) -> untyped?
44
47
  def singleton_method: (class_name: String | Symbol, method_name: String | Symbol) -> untyped?
48
+ def uncached_singleton_definition: (String | Symbol class_name) -> untyped?
45
49
  def class_type_param_names: (String | Symbol class_name) -> Array[Symbol]
46
50
  def class_ordering: (String | Symbol lhs, String | Symbol rhs) -> ordering
47
51
  def constant_type: (String name) -> Type::t?
52
+ def constant_names: () -> Array[String]
53
+ def each_known_class_name: () { (String name) -> void } -> untyped
48
54
  end
49
55
 
50
56
  class RbsHierarchy
@@ -86,6 +86,7 @@ module Rigor
86
86
  def self?.narrow_class: (Type::t type, String class_name, ?exact: bool, ?environment: Environment) -> Type::t
87
87
  def self?.narrow_not_class: (Type::t type, String class_name, ?exact: bool, ?environment: Environment) -> Type::t
88
88
  def self?.narrow_not_refinement: (Type::t current_type, Type::t refinement_type) -> Type::t
89
+ def self?.narrow_for_fact: (Type::t current, untyped fact, untyped environment) -> Type::t
89
90
  def self?.predicate_scopes: (untyped node, Scope scope) -> [Scope, Scope]
90
91
  def self?.case_when_scopes: (untyped subject, Array[untyped] conditions, Scope scope) -> [Scope, Scope]
91
92
  def self?.analyse: (untyped node, Scope scope) -> untyped
@@ -43,6 +43,8 @@ module Rigor
43
43
  def self?.read_return_type_override: (untyped method_def) -> Type::t?
44
44
  def self?.parse_return_type_override: (String string) -> Type::t?
45
45
 
46
+ def self?.read_flow_contribution: (untyped method_def) -> untyped?
47
+
46
48
  class ParamOverride
47
49
  attr_reader param_name: Symbol
48
50
  attr_reader type: Type::t