rigortype 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +116 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +123 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +118 -0
- data/lib/rigor/analysis/check_rules.rb +346 -18
- data/lib/rigor/analysis/rule_catalog.rb +343 -0
- data/lib/rigor/analysis/runner.rb +90 -6
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +29 -5
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +186 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/inference/expression_typer.rb +3 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +43 -2
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/narrowing.rb +150 -6
- data/lib/rigor/inference/scope_indexer.rb +220 -17
- data/lib/rigor/inference/statement_evaluator.rb +29 -0
- data/lib/rigor/plugin/base.rb +43 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/load_error.rb +14 -2
- data/lib/rigor/plugin/loader.rb +116 -0
- data/lib/rigor/plugin/manifest.rb +75 -6
- data/lib/rigor/plugin/services.rb +14 -2
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +8 -2
- metadata +9 -1
|
@@ -11,12 +11,24 @@ module Rigor
|
|
|
11
11
|
# to be isolated at the analyzer boundary; this class is the
|
|
12
12
|
# carrier for that contract on the loading side.
|
|
13
13
|
class LoadError < StandardError
|
|
14
|
-
attr_reader :plugin_ref, :cause_class
|
|
14
|
+
attr_reader :plugin_ref, :cause_class, :reason
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# ADR-9 slice 5 introduces two new reason codes alongside the
|
|
17
|
+
# implicit "load failure" used for require / configuration /
|
|
18
|
+
# init failures:
|
|
19
|
+
#
|
|
20
|
+
# - `:missing-producer` — a non-optional `manifest(consumes:)`
|
|
21
|
+
# entry names a `(plugin_id, name)` no loaded plugin
|
|
22
|
+
# produces.
|
|
23
|
+
# - `:dependency-cycle` — the consumes graph forms a cycle.
|
|
24
|
+
#
|
|
25
|
+
# Older callers omit `reason:` and the field defaults to nil
|
|
26
|
+
# (the legacy "load failure" envelope).
|
|
27
|
+
def initialize(message, plugin_ref:, cause: nil, reason: nil)
|
|
17
28
|
super(message)
|
|
18
29
|
@plugin_ref = plugin_ref
|
|
19
30
|
@cause_class = cause&.class
|
|
31
|
+
@reason = reason&.to_sym
|
|
20
32
|
end
|
|
21
33
|
end
|
|
22
34
|
end
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -63,6 +63,15 @@ module Rigor
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# ADR-9 slice 5 — topological sort by `manifest(consumes:)`
|
|
67
|
+
# so producers run before consumers, plus early
|
|
68
|
+
# `missing-producer` validation. Cycles surface as
|
|
69
|
+
# `dependency-cycle` LoadErrors. When validation fails, the
|
|
70
|
+
# offending plugin(s) drop from the returned plugins list
|
|
71
|
+
# and the LoadError surfaces alongside any earlier failure.
|
|
72
|
+
plugins, sort_errors = topo_sort_plugins(plugins)
|
|
73
|
+
load_errors.concat(sort_errors)
|
|
74
|
+
|
|
66
75
|
Registry.new(plugins: plugins, load_errors: load_errors)
|
|
67
76
|
end
|
|
68
77
|
|
|
@@ -186,6 +195,113 @@ module Rigor
|
|
|
186
195
|
rescue StandardError
|
|
187
196
|
plugin_class.to_s
|
|
188
197
|
end
|
|
198
|
+
|
|
199
|
+
# ADR-9 slice 5 — topological sort of plugins by their
|
|
200
|
+
# `manifest(consumes:)` declarations. Returns `[sorted_plugins,
|
|
201
|
+
# load_errors]`. Determinism: when no dependency relation
|
|
202
|
+
# forces an order, plugins are visited alphabetically by
|
|
203
|
+
# manifest id. A non-optional consume of a `(plugin_id, name)`
|
|
204
|
+
# whose producer is missing emits a `:missing-producer`
|
|
205
|
+
# LoadError and drops the consumer; cycles emit a
|
|
206
|
+
# `:dependency-cycle` LoadError naming the offending chain.
|
|
207
|
+
def topo_sort_plugins(plugins)
|
|
208
|
+
# If no plugin opts into the cross-plugin API the loader's
|
|
209
|
+
# legacy configuration-order contract is preserved
|
|
210
|
+
# unchanged. Topo sort and missing-producer validation only
|
|
211
|
+
# run when at least one plugin declares `consumes:`.
|
|
212
|
+
return [plugins, []] unless plugins.any? { |p| p.manifest.consumes.any? }
|
|
213
|
+
|
|
214
|
+
index = plugins.to_h { |plugin| [plugin.manifest.id, plugin] }
|
|
215
|
+
errors = validate_missing_producers(plugins, index)
|
|
216
|
+
sortable = plugins.reject { |p| errors.any? { |e| e.plugin_ref == p.manifest.id } }
|
|
217
|
+
config_order = plugins.each_with_index.to_h { |plugin, i| [plugin.manifest.id, i] }
|
|
218
|
+
|
|
219
|
+
sort_in_topo_order(sortable, index, errors, config_order)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def validate_missing_producers(plugins, index)
|
|
223
|
+
errors = []
|
|
224
|
+
plugins.each do |plugin|
|
|
225
|
+
plugin.manifest.consumes.each do |consume|
|
|
226
|
+
next if consume.optional
|
|
227
|
+
next if index.key?(consume.plugin_id) && producer_provides?(index[consume.plugin_id], consume.name)
|
|
228
|
+
|
|
229
|
+
errors << LoadError.new(
|
|
230
|
+
"plugin #{plugin.manifest.id.inspect} consumes " \
|
|
231
|
+
"#{consume.plugin_id.inspect}/#{consume.name} but no loaded plugin " \
|
|
232
|
+
"with that id declares `produces: [#{consume.name.inspect}]`",
|
|
233
|
+
plugin_ref: plugin.manifest.id,
|
|
234
|
+
reason: :"missing-producer"
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
errors
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def producer_provides?(producer, name)
|
|
242
|
+
producer.manifest.produces.include?(name)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Kahn's algorithm with `Configuration#plugins`-order
|
|
246
|
+
# tie-break. Edges go from producer -> consumer (producer
|
|
247
|
+
# must visit first). When two plugins are simultaneously
|
|
248
|
+
# ready, the configuration-order index decides the visit
|
|
249
|
+
# order — preserves the v0.1.0 legacy contract for plugins
|
|
250
|
+
# without dependencies.
|
|
251
|
+
def sort_in_topo_order(plugins, index, errors, config_order)
|
|
252
|
+
in_degree, forward = build_consumes_graph(plugins, index, errors)
|
|
253
|
+
ordered, cycle_errors = kahn_walk(plugins, in_degree, forward, config_order)
|
|
254
|
+
[ordered, errors + cycle_errors]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def build_consumes_graph(plugins, index, errors)
|
|
258
|
+
in_degree = Hash.new(0)
|
|
259
|
+
forward = Hash.new { |h, k| h[k] = [] }
|
|
260
|
+
plugins.each do |consumer|
|
|
261
|
+
consumer.manifest.consumes.each do |consume|
|
|
262
|
+
next unless index.key?(consume.plugin_id)
|
|
263
|
+
next if errors.any? { |e| e.plugin_ref == consume.plugin_id }
|
|
264
|
+
|
|
265
|
+
forward[consume.plugin_id] << consumer.manifest.id
|
|
266
|
+
in_degree[consumer.manifest.id] += 1
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
[in_degree, forward]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def kahn_walk(plugins, in_degree, forward, config_order)
|
|
273
|
+
order = ->(plugin) { config_order.fetch(plugin.manifest.id, Float::INFINITY) }
|
|
274
|
+
ready = plugins.select { |p| in_degree[p.manifest.id].zero? }.sort_by(&order)
|
|
275
|
+
result = kahn_collect(plugins, in_degree, forward, ready, order)
|
|
276
|
+
|
|
277
|
+
return [result, []] if result.size == plugins.size
|
|
278
|
+
|
|
279
|
+
cycled = plugins - result
|
|
280
|
+
[result, [dependency_cycle_error(cycled)]]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def kahn_collect(plugins, in_degree, forward, ready, order)
|
|
284
|
+
result = []
|
|
285
|
+
until ready.empty?
|
|
286
|
+
plugin = ready.shift
|
|
287
|
+
result << plugin
|
|
288
|
+
forward[plugin.manifest.id].each do |consumer_id|
|
|
289
|
+
in_degree[consumer_id] -= 1
|
|
290
|
+
ready << plugins.find { |p| p.manifest.id == consumer_id } if in_degree[consumer_id].zero?
|
|
291
|
+
end
|
|
292
|
+
ready.sort_by!(&order)
|
|
293
|
+
end
|
|
294
|
+
result
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def dependency_cycle_error(cycled)
|
|
298
|
+
ids = cycled.map { |p| p.manifest.id }.sort
|
|
299
|
+
LoadError.new(
|
|
300
|
+
"plugin dependency cycle through `manifest(consumes:)`: #{ids.inspect}",
|
|
301
|
+
plugin_ref: ids.first,
|
|
302
|
+
reason: :"dependency-cycle"
|
|
303
|
+
)
|
|
304
|
+
end
|
|
189
305
|
end
|
|
190
306
|
end
|
|
191
307
|
end
|
|
@@ -11,7 +11,7 @@ module Rigor
|
|
|
11
11
|
# The fields are pinned by ADR-2 § "Registration, Configuration,
|
|
12
12
|
# and Caching"; the v0.1.0 plugin contract surface treats this
|
|
13
13
|
# struct as the public manifest shape.
|
|
14
|
-
class Manifest
|
|
14
|
+
class Manifest # rubocop:disable Metrics/ClassLength
|
|
15
15
|
# Same regex {Rigor::Cache::Store::VALID_PRODUCER_ID} uses,
|
|
16
16
|
# so plugin ids round-trip through cache producer ids and
|
|
17
17
|
# `plugin.<id>.<rule>` diagnostic identifiers without escape.
|
|
@@ -23,23 +23,51 @@ module Rigor
|
|
|
23
23
|
# the v0.1.0 protocol slices need them.
|
|
24
24
|
VALID_VALUE_KINDS = %i[string boolean integer array hash any].freeze
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# ADR-9 slice 4 — declared cross-plugin fact dependencies.
|
|
27
|
+
# `produces:` lists the names this plugin publishes through
|
|
28
|
+
# its `#prepare(services)` hook. `consumes:` lists the
|
|
29
|
+
# `(plugin_id, name)` pairs this plugin reads from
|
|
30
|
+
# `services.fact_store`. The loader uses both for
|
|
31
|
+
# topological sort + missing-producer detection (slice 5);
|
|
32
|
+
# slice 4 carries the declarations on the manifest but the
|
|
33
|
+
# loader does not yet enforce them.
|
|
34
|
+
Consumption = Data.define(:plugin_id, :name, :optional) do
|
|
35
|
+
def initialize(plugin_id:, name:, optional: false)
|
|
36
|
+
super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes
|
|
41
|
+
|
|
42
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
43
|
+
id:, version:,
|
|
44
|
+
description: nil, protocols: [], config_schema: {},
|
|
45
|
+
produces: [], consumes: []
|
|
46
|
+
)
|
|
29
47
|
validate_id!(id)
|
|
30
48
|
validate_version!(version)
|
|
31
49
|
validate_protocols!(protocols)
|
|
32
50
|
validate_config_schema!(config_schema)
|
|
51
|
+
validate_produces!(produces)
|
|
52
|
+
|
|
53
|
+
assign_fields(id, version, description, protocols, config_schema, produces, consumes)
|
|
54
|
+
freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
33
58
|
|
|
59
|
+
def assign_fields(id, version, description, protocols, config_schema, produces, consumes) # rubocop:disable Metrics/ParameterLists
|
|
34
60
|
@id = id.dup.freeze
|
|
35
61
|
@version = version.dup.freeze
|
|
36
62
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
37
63
|
@protocols = protocols.map(&:to_sym).freeze
|
|
38
64
|
@config_schema = config_schema.to_h { |k, v| [k.to_s.dup.freeze, v.to_sym] }.freeze
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
@produces = produces.map(&:to_sym).freeze
|
|
66
|
+
@consumes = coerce_consumes(consumes)
|
|
41
67
|
end
|
|
42
68
|
|
|
69
|
+
public
|
|
70
|
+
|
|
43
71
|
# Validates the user-supplied plugin config block against this
|
|
44
72
|
# manifest's `config_schema`. Returns an array of human-readable
|
|
45
73
|
# error strings (empty when the config is valid). Slice 1 checks
|
|
@@ -69,7 +97,9 @@ module Rigor
|
|
|
69
97
|
"version" => version,
|
|
70
98
|
"description" => description,
|
|
71
99
|
"protocols" => protocols.map(&:to_s),
|
|
72
|
-
"config_schema" => config_schema.to_h { |k, v| [k, v.to_s] }
|
|
100
|
+
"config_schema" => config_schema.to_h { |k, v| [k, v.to_s] },
|
|
101
|
+
"produces" => produces.map(&:to_s),
|
|
102
|
+
"consumes" => consumes.map { |c| consumption_hash(c) }
|
|
73
103
|
}
|
|
74
104
|
end
|
|
75
105
|
|
|
@@ -129,6 +159,45 @@ module Rigor
|
|
|
129
159
|
else false
|
|
130
160
|
end
|
|
131
161
|
end
|
|
162
|
+
|
|
163
|
+
def validate_produces!(produces)
|
|
164
|
+
return if produces.is_a?(Array) && produces.all? { |p| p.is_a?(Symbol) || p.is_a?(String) }
|
|
165
|
+
|
|
166
|
+
raise ArgumentError, "plugin manifest produces must be an Array of Symbol/String, got #{produces.inspect}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def coerce_consumes(consumes)
|
|
170
|
+
unless consumes.is_a?(Array)
|
|
171
|
+
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
consumes.map { |entry| coerce_consumption(entry) }.freeze
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def coerce_consumption(entry)
|
|
178
|
+
case entry
|
|
179
|
+
when Consumption then entry
|
|
180
|
+
when Hash then build_consumption_from_hash(entry)
|
|
181
|
+
else raise ArgumentError,
|
|
182
|
+
"plugin manifest consumes entry must be a Hash or Consumption, got #{entry.inspect}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def consumption_hash(consumption)
|
|
187
|
+
{ "plugin_id" => consumption.plugin_id, "name" => consumption.name.to_s, "optional" => consumption.optional }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_consumption_from_hash(entry)
|
|
191
|
+
plugin_id = entry[:plugin_id] || entry["plugin_id"]
|
|
192
|
+
name = entry[:name] || entry["name"]
|
|
193
|
+
optional = entry.key?(:optional) ? entry[:optional] : entry["optional"]
|
|
194
|
+
if plugin_id.nil? || name.nil?
|
|
195
|
+
raise ArgumentError,
|
|
196
|
+
"plugin manifest consumes entry missing plugin_id/name: #{entry.inspect}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
Consumption.new(plugin_id: plugin_id, name: name, optional: optional || false)
|
|
200
|
+
end
|
|
132
201
|
end
|
|
133
202
|
end
|
|
134
203
|
end
|
|
@@ -31,15 +31,27 @@ module Rigor
|
|
|
31
31
|
# raw `File.read` so reads stay within the trusted scope and
|
|
32
32
|
# feed cache invalidation; ADR-2 § "Plugin Trust and I/O
|
|
33
33
|
# Policy" documents the trust model the boundary enforces.
|
|
34
|
+
#
|
|
35
|
+
# ADR-9 slice 2 adds `fact_store`: the per-run cross-plugin
|
|
36
|
+
# `Plugin::FactStore`. Producer plugins publish their facts
|
|
37
|
+
# in `#prepare(services)` (slice 3); consumer plugins read in
|
|
38
|
+
# `#diagnostics_for_file` via `services.fact_store.read(...)`.
|
|
39
|
+
# A fresh `FactStore` instance is constructed per Services
|
|
40
|
+
# when none is supplied — the runner threads its own instance
|
|
41
|
+
# in once slice 3 wires `#prepare` invocation.
|
|
34
42
|
class Services
|
|
35
|
-
attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy
|
|
43
|
+
attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy, :fact_store
|
|
36
44
|
|
|
37
|
-
def initialize(
|
|
45
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
46
|
+
reflection:, type:, configuration:,
|
|
47
|
+
cache_store: nil, trust_policy: nil, fact_store: nil
|
|
48
|
+
)
|
|
38
49
|
@reflection = reflection
|
|
39
50
|
@type = type
|
|
40
51
|
@configuration = configuration
|
|
41
52
|
@cache_store = cache_store
|
|
42
53
|
@trust_policy = trust_policy || default_trust_policy
|
|
54
|
+
@fact_store = fact_store || FactStore.new
|
|
43
55
|
freeze
|
|
44
56
|
end
|
|
45
57
|
|
|
@@ -32,15 +32,18 @@ module Rigor
|
|
|
32
32
|
# `Gemfile.lock`, and each trusted gem's
|
|
33
33
|
# `Gem::Specification#full_gem_path`. The user extends this
|
|
34
34
|
# with `.rigor.yml`'s `plugins_io.allowed_paths:`.
|
|
35
|
-
# - `network_policy`:
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
# - `network_policy`: one of {VALID_NETWORK_POLICIES}.
|
|
36
|
+
# `:disabled` (default) makes {IoBoundary#open_url} always
|
|
37
|
+
# raise. `:allowlist` (v0.1.2) consults `allowed_url_hosts`
|
|
38
|
+
# on every fetch — the hostname must be on the list and
|
|
39
|
+
# the URL scheme MUST be `https`. The list of allowed hosts
|
|
40
|
+
# is exact-match (no wildcards in v0.1.2).
|
|
38
41
|
class TrustPolicy
|
|
39
|
-
VALID_NETWORK_POLICIES = %i[disabled].freeze
|
|
42
|
+
VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
|
|
40
43
|
|
|
41
|
-
attr_reader :trusted_gems, :allowed_read_roots, :network_policy
|
|
44
|
+
attr_reader :trusted_gems, :allowed_read_roots, :network_policy, :allowed_url_hosts
|
|
42
45
|
|
|
43
|
-
def initialize(trusted_gems: [], allowed_read_roots: [], network_policy: :disabled)
|
|
46
|
+
def initialize(trusted_gems: [], allowed_read_roots: [], network_policy: :disabled, allowed_url_hosts: [])
|
|
44
47
|
validate_network_policy!(network_policy)
|
|
45
48
|
|
|
46
49
|
@trusted_gems = trusted_gems.map { |g| g.to_s.dup.freeze }.uniq.sort.freeze
|
|
@@ -50,6 +53,7 @@ module Rigor
|
|
|
50
53
|
.sort
|
|
51
54
|
.freeze
|
|
52
55
|
@network_policy = network_policy
|
|
56
|
+
@allowed_url_hosts = allowed_url_hosts.map { |h| h.to_s.downcase.dup.freeze }.uniq.sort.freeze
|
|
53
57
|
freeze
|
|
54
58
|
end
|
|
55
59
|
|
|
@@ -67,6 +71,24 @@ module Rigor
|
|
|
67
71
|
@network_policy != :disabled
|
|
68
72
|
end
|
|
69
73
|
|
|
74
|
+
# @param url [String, URI]
|
|
75
|
+
# @return [Boolean] true when the URL scheme is `https` and
|
|
76
|
+
# the parsed hostname is in `allowed_url_hosts`. Always
|
|
77
|
+
# `false` while `network_policy` is `:disabled`.
|
|
78
|
+
def allow_url?(url)
|
|
79
|
+
return false if @network_policy == :disabled
|
|
80
|
+
return false if @allowed_url_hosts.empty?
|
|
81
|
+
|
|
82
|
+
require "uri"
|
|
83
|
+
uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
|
|
84
|
+
return false unless uri.is_a?(URI::HTTPS)
|
|
85
|
+
return false if uri.host.nil?
|
|
86
|
+
|
|
87
|
+
@allowed_url_hosts.include?(uri.host.downcase)
|
|
88
|
+
rescue URI::InvalidURIError
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
70
92
|
def gem_trusted?(name)
|
|
71
93
|
@trusted_gems.include?(name.to_s)
|
|
72
94
|
end
|
|
@@ -75,7 +97,8 @@ module Rigor
|
|
|
75
97
|
{
|
|
76
98
|
"trusted_gems" => trusted_gems,
|
|
77
99
|
"allowed_read_roots" => allowed_read_roots,
|
|
78
|
-
"network_policy" => network_policy.to_s
|
|
100
|
+
"network_policy" => network_policy.to_s,
|
|
101
|
+
"allowed_url_hosts" => allowed_url_hosts
|
|
79
102
|
}
|
|
80
103
|
end
|
|
81
104
|
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "plugin/manifest"
|
|
|
4
4
|
require_relative "plugin/access_denied_error"
|
|
5
5
|
require_relative "plugin/trust_policy"
|
|
6
6
|
require_relative "plugin/io_boundary"
|
|
7
|
+
require_relative "plugin/fact_store"
|
|
7
8
|
require_relative "plugin/services"
|
|
8
9
|
require_relative "plugin/base"
|
|
9
10
|
require_relative "plugin/registry"
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Rigor
|
|
|
20
20
|
:ivars, :cvars, :globals,
|
|
21
21
|
:class_ivars, :class_cvars, :program_globals,
|
|
22
22
|
:discovered_classes, :in_source_constants, :discovered_methods,
|
|
23
|
-
:discovered_def_nodes
|
|
23
|
+
:discovered_def_nodes, :discovered_method_visibilities
|
|
24
24
|
|
|
25
25
|
EMPTY_DECLARED_TYPES = {}.compare_by_identity.freeze
|
|
26
26
|
EMPTY_VAR_BINDINGS = {}.freeze
|
|
@@ -47,7 +47,8 @@ module Rigor
|
|
|
47
47
|
discovered_classes: EMPTY_VAR_BINDINGS,
|
|
48
48
|
in_source_constants: EMPTY_VAR_BINDINGS,
|
|
49
49
|
discovered_methods: EMPTY_CLASS_BINDINGS,
|
|
50
|
-
discovered_def_nodes: EMPTY_CLASS_BINDINGS
|
|
50
|
+
discovered_def_nodes: EMPTY_CLASS_BINDINGS,
|
|
51
|
+
discovered_method_visibilities: EMPTY_CLASS_BINDINGS
|
|
51
52
|
)
|
|
52
53
|
@environment = environment
|
|
53
54
|
@locals = locals
|
|
@@ -64,6 +65,7 @@ module Rigor
|
|
|
64
65
|
@in_source_constants = in_source_constants
|
|
65
66
|
@discovered_methods = discovered_methods
|
|
66
67
|
@discovered_def_nodes = discovered_def_nodes
|
|
68
|
+
@discovered_method_visibilities = discovered_method_visibilities
|
|
67
69
|
freeze
|
|
68
70
|
end
|
|
69
71
|
|
|
@@ -268,6 +270,26 @@ module Rigor
|
|
|
268
270
|
rebuild(discovered_def_nodes: table)
|
|
269
271
|
end
|
|
270
272
|
|
|
273
|
+
# v0.1.2 — per-class table mapping `method_name (Symbol) →
|
|
274
|
+
# :public | :private | :protected`. Populated by
|
|
275
|
+
# `ScopeIndexer` for every `def` it sees inside a class
|
|
276
|
+
# body, with the visibility taken from the surrounding
|
|
277
|
+
# `private` / `protected` / `public` modifier state plus
|
|
278
|
+
# any post-hoc `private :name, ...` named-argument calls.
|
|
279
|
+
# Consumed by the `def.method-visibility-mismatch` rule
|
|
280
|
+
# so explicit-non-self calls to a private method surface
|
|
281
|
+
# a diagnostic.
|
|
282
|
+
def discovered_method_visibility(class_name, method_name)
|
|
283
|
+
table = @discovered_method_visibilities[class_name.to_s]
|
|
284
|
+
return nil unless table
|
|
285
|
+
|
|
286
|
+
table[method_name.to_sym]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def with_discovered_method_visibilities(table)
|
|
290
|
+
rebuild(discovered_method_visibilities: table)
|
|
291
|
+
end
|
|
292
|
+
|
|
271
293
|
def facts_for(target: nil, bucket: nil)
|
|
272
294
|
fact_store.facts_for(target: target, bucket: bucket)
|
|
273
295
|
end
|
|
@@ -334,7 +356,8 @@ module Rigor
|
|
|
334
356
|
declared_types: @declared_types, ivars: @ivars, cvars: @cvars, globals: @globals,
|
|
335
357
|
class_ivars: @class_ivars, class_cvars: @class_cvars, program_globals: @program_globals,
|
|
336
358
|
discovered_classes: @discovered_classes, in_source_constants: @in_source_constants,
|
|
337
|
-
discovered_methods: @discovered_methods, discovered_def_nodes: @discovered_def_nodes
|
|
359
|
+
discovered_methods: @discovered_methods, discovered_def_nodes: @discovered_def_nodes,
|
|
360
|
+
discovered_method_visibilities: @discovered_method_visibilities
|
|
338
361
|
)
|
|
339
362
|
self.class.new(
|
|
340
363
|
environment: environment, locals: locals,
|
|
@@ -346,7 +369,8 @@ module Rigor
|
|
|
346
369
|
discovered_classes: discovered_classes,
|
|
347
370
|
in_source_constants: in_source_constants,
|
|
348
371
|
discovered_methods: discovered_methods,
|
|
349
|
-
discovered_def_nodes: discovered_def_nodes
|
|
372
|
+
discovered_def_nodes: discovered_def_nodes,
|
|
373
|
+
discovered_method_visibilities: discovered_method_visibilities
|
|
350
374
|
)
|
|
351
375
|
end
|
|
352
376
|
|
|
@@ -371,7 +395,8 @@ module Rigor
|
|
|
371
395
|
discovered_classes: discovered_classes,
|
|
372
396
|
in_source_constants: in_source_constants,
|
|
373
397
|
discovered_methods: discovered_methods,
|
|
374
|
-
discovered_def_nodes: discovered_def_nodes
|
|
398
|
+
discovered_def_nodes: discovered_def_nodes,
|
|
399
|
+
discovered_method_visibilities: discovered_method_visibilities
|
|
375
400
|
)
|
|
376
401
|
end
|
|
377
402
|
end
|
data/lib/rigor/trinary.rb
CHANGED
|
@@ -66,12 +66,16 @@ module Rigor
|
|
|
66
66
|
# `:neg_infinity` directly with an `Integer`.
|
|
67
67
|
def lower
|
|
68
68
|
m = min
|
|
69
|
-
m.is_a?(
|
|
69
|
+
return m if m.is_a?(Integer)
|
|
70
|
+
|
|
71
|
+
-Float::INFINITY
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def upper
|
|
73
75
|
m = max
|
|
74
|
-
m.is_a?(
|
|
76
|
+
return m if m.is_a?(Integer)
|
|
77
|
+
|
|
78
|
+
Float::INFINITY
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
ALIAS_NAMES = {
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/environment.rbs
CHANGED
|
@@ -6,11 +6,12 @@ module Rigor
|
|
|
6
6
|
|
|
7
7
|
attr_reader class_registry: ClassRegistry
|
|
8
8
|
attr_reader rbs_loader: RbsLoader?
|
|
9
|
+
attr_reader plugin_registry: untyped?
|
|
9
10
|
|
|
10
11
|
def self.default: () -> Environment
|
|
11
|
-
def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?) -> Environment
|
|
12
|
+
def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?, ?cache_store: untyped?, ?plugin_registry: untyped?) -> Environment
|
|
12
13
|
|
|
13
|
-
def initialize: (?class_registry: ClassRegistry, ?rbs_loader: RbsLoader?) -> void
|
|
14
|
+
def initialize: (?class_registry: ClassRegistry, ?rbs_loader: RbsLoader?, ?plugin_registry: untyped?) -> void
|
|
14
15
|
def nominal_for_name: (String | Symbol name) -> Type::Nominal?
|
|
15
16
|
def singleton_for_name: (String | Symbol name) -> Type::Singleton?
|
|
16
17
|
def constant_for_name: (String | Symbol name) -> Type::t?
|
data/sig/rigor/scope.rbs
CHANGED
|
@@ -15,6 +15,7 @@ module Rigor
|
|
|
15
15
|
attr_reader in_source_constants: Hash[String, Type::t]
|
|
16
16
|
attr_reader discovered_methods: Hash[String, Hash[Symbol, Symbol]]
|
|
17
17
|
attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
|
|
18
|
+
attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
|
|
18
19
|
|
|
19
20
|
def self.empty: (?environment: Environment) -> Scope
|
|
20
21
|
|
|
@@ -39,6 +40,8 @@ module Rigor
|
|
|
39
40
|
def with_discovered_def_nodes: (Hash[String, Hash[Symbol, untyped]] table) -> Scope
|
|
40
41
|
def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
41
42
|
def top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
43
|
+
def with_discovered_method_visibilities: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
|
|
44
|
+
def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
|
|
42
45
|
def with_fact: (Analysis::FactStore::Fact fact) -> Scope
|
|
43
46
|
def with_self_type: (Type::t? type) -> Scope
|
|
44
47
|
def with_declared_types: (Hash[untyped, Type::t] table) -> Scope
|
data/sig/rigor.rbs
CHANGED
|
@@ -64,8 +64,14 @@ module Rigor
|
|
|
64
64
|
class Runner
|
|
65
65
|
RUBY_GLOB: String
|
|
66
66
|
DEFAULT_CACHE_ROOT: String
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# `Rigor::Cache::Store` itself is not yet sig-covered (the
|
|
68
|
+
# cache namespace is in `UNSIGNED_NAMESPACES` per
|
|
69
|
+
# `spec/rigor/public_api_drift_spec.rb`), so reference it as
|
|
70
|
+
# `untyped` until the full Cache::Store sig lands. Steep
|
|
71
|
+
# otherwise raises `RBS::UnknownTypeName` for the named type.
|
|
72
|
+
attr_reader cache_store: untyped
|
|
73
|
+
attr_reader plugin_registry: untyped
|
|
74
|
+
def initialize: (configuration: Configuration, ?explain: bool, ?cache_store: untyped, ?plugin_requirer: untyped) -> void
|
|
69
75
|
def run: (?Array[String] paths) -> Result
|
|
70
76
|
end
|
|
71
77
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
@@ -184,13 +184,18 @@ files:
|
|
|
184
184
|
- exe/rigor
|
|
185
185
|
- lib/rigor.rb
|
|
186
186
|
- lib/rigor/analysis/check_rules.rb
|
|
187
|
+
- lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb
|
|
188
|
+
- lib/rigor/analysis/check_rules/dead_assignment_collector.rb
|
|
189
|
+
- lib/rigor/analysis/check_rules/ivar_write_collector.rb
|
|
187
190
|
- lib/rigor/analysis/diagnostic.rb
|
|
188
191
|
- lib/rigor/analysis/fact_store.rb
|
|
189
192
|
- lib/rigor/analysis/result.rb
|
|
193
|
+
- lib/rigor/analysis/rule_catalog.rb
|
|
190
194
|
- lib/rigor/analysis/runner.rb
|
|
191
195
|
- lib/rigor/ast.rb
|
|
192
196
|
- lib/rigor/ast/type_node.rb
|
|
193
197
|
- lib/rigor/builtins/imported_refinements.rb
|
|
198
|
+
- lib/rigor/builtins/regex_refinement.rb
|
|
194
199
|
- lib/rigor/cache/descriptor.rb
|
|
195
200
|
- lib/rigor/cache/rbs_class_ancestor_table.rb
|
|
196
201
|
- lib/rigor/cache/rbs_class_type_param_names.rb
|
|
@@ -202,6 +207,8 @@ files:
|
|
|
202
207
|
- lib/rigor/cache/rbs_known_class_names.rb
|
|
203
208
|
- lib/rigor/cache/store.rb
|
|
204
209
|
- lib/rigor/cli.rb
|
|
210
|
+
- lib/rigor/cli/diff_command.rb
|
|
211
|
+
- lib/rigor/cli/explain_command.rb
|
|
205
212
|
- lib/rigor/cli/type_of_command.rb
|
|
206
213
|
- lib/rigor/cli/type_of_renderer.rb
|
|
207
214
|
- lib/rigor/cli/type_scan_command.rb
|
|
@@ -265,6 +272,7 @@ files:
|
|
|
265
272
|
- lib/rigor/plugin.rb
|
|
266
273
|
- lib/rigor/plugin/access_denied_error.rb
|
|
267
274
|
- lib/rigor/plugin/base.rb
|
|
275
|
+
- lib/rigor/plugin/fact_store.rb
|
|
268
276
|
- lib/rigor/plugin/io_boundary.rb
|
|
269
277
|
- lib/rigor/plugin/load_error.rb
|
|
270
278
|
- lib/rigor/plugin/loader.rb
|