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
|
@@ -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
|
data/lib/rigor/plugin.rb
ADDED
|
@@ -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"
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -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
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
data/lib/rigor/type/refined.rb
CHANGED
|
@@ -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
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"
|
data/sig/rigor/environment.rbs
CHANGED
|
@@ -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
|
data/sig/rigor/inference.rbs
CHANGED
|
@@ -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
|
data/sig/rigor/rbs_extended.rbs
CHANGED
|
@@ -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
|