rigortype 0.1.1 → 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/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- 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 +2 -1
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +10 -4
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/scope_indexer.rb +171 -2
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/scope.rbs +3 -0
- metadata +7 -1
|
@@ -27,20 +27,37 @@ module Rigor
|
|
|
27
27
|
# the file's contents, and adds a digest-keyed
|
|
28
28
|
# {Cache::Descriptor::FileEntry} to the boundary's
|
|
29
29
|
# accumulated descriptor.
|
|
30
|
-
# - `#open_url(url)` —
|
|
31
|
-
# `network_policy
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# - `#open_url(url)` — fetches the URL when the policy
|
|
31
|
+
# permits it (`network_policy: :allowlist` plus an
|
|
32
|
+
# `allowed_url_hosts` match) and raises
|
|
33
|
+
# {AccessDeniedError} otherwise. v0.1.2 ships the
|
|
34
|
+
# allowlist surface; the default project policy still
|
|
35
|
+
# has `network_policy: :disabled` so plugins that want
|
|
36
|
+
# network access opt in explicitly through
|
|
37
|
+
# `.rigor.yml`'s `plugins_io.network: allowlist` plus
|
|
38
|
+
# `plugins_io.allowed_url_hosts: [...]`. The HTTP fetch
|
|
39
|
+
# is GET-only over HTTPS, capped at {URL_TIMEOUT_SECONDS}
|
|
40
|
+
# wall time and {URL_MAX_BYTES} body size; non-2xx
|
|
41
|
+
# responses raise {AccessDeniedError} so plugin code
|
|
42
|
+
# doesn't have to rescue mid-build.
|
|
34
43
|
# - `#cache_descriptor` — flushes the accumulated entries into
|
|
35
44
|
# a fresh {Cache::Descriptor} for the contribution that
|
|
36
|
-
# built it.
|
|
45
|
+
# built it. URL fetches contribute `ConfigEntry` rows
|
|
46
|
+
# keyed `"url:#{url}"` with the response body's SHA-256
|
|
47
|
+
# so contributions invalidate when the remote document
|
|
48
|
+
# changes.
|
|
37
49
|
class IoBoundary
|
|
50
|
+
URL_TIMEOUT_SECONDS = 10
|
|
51
|
+
URL_MAX_BYTES = 10 * 1024 * 1024
|
|
52
|
+
|
|
38
53
|
attr_reader :policy, :plugin_id
|
|
39
54
|
|
|
40
|
-
def initialize(policy:, plugin_id:)
|
|
55
|
+
def initialize(policy:, plugin_id:, http_client: DefaultHttpClient.new)
|
|
41
56
|
@policy = policy
|
|
42
57
|
@plugin_id = plugin_id.to_s.dup.freeze
|
|
43
58
|
@file_entries = {}
|
|
59
|
+
@config_entries = {}
|
|
60
|
+
@http_client = http_client
|
|
44
61
|
@mutex = Mutex.new
|
|
45
62
|
end
|
|
46
63
|
|
|
@@ -64,30 +81,39 @@ module Rigor
|
|
|
64
81
|
contents
|
|
65
82
|
end
|
|
66
83
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# the
|
|
70
|
-
# the
|
|
84
|
+
# Fetches the URL when the policy permits it. Returns the
|
|
85
|
+
# response body. Raises {AccessDeniedError} when the policy
|
|
86
|
+
# is `:disabled`, the URL scheme is not `https`, the host is
|
|
87
|
+
# not on the allowlist, the response is non-2xx, the body
|
|
88
|
+
# exceeds {URL_MAX_BYTES}, or the request times out
|
|
89
|
+
# ({URL_TIMEOUT_SECONDS}). On success, records a
|
|
90
|
+
# `ConfigEntry` keyed `"url:#{url}"` with the body's
|
|
91
|
+
# SHA-256 so the cache descriptor invalidates if the remote
|
|
92
|
+
# document changes.
|
|
71
93
|
def open_url(url)
|
|
72
|
-
|
|
94
|
+
url_string = url.to_s
|
|
95
|
+
unless @policy.allow_url?(url_string)
|
|
73
96
|
raise AccessDeniedError.new(
|
|
74
97
|
"plugin #{@plugin_id.inspect} cannot open URL #{url.inspect}: " \
|
|
75
|
-
"
|
|
98
|
+
"URL is not permitted by the active TrustPolicy " \
|
|
99
|
+
"(network_policy=#{@policy.network_policy} allowed_url_hosts=#{@policy.allowed_url_hosts.inspect})",
|
|
76
100
|
reason: :network_disabled,
|
|
77
|
-
resource:
|
|
101
|
+
resource: url_string
|
|
78
102
|
)
|
|
79
103
|
end
|
|
80
104
|
|
|
81
|
-
|
|
105
|
+
body = @http_client.get(url_string, timeout: URL_TIMEOUT_SECONDS, max_bytes: URL_MAX_BYTES)
|
|
106
|
+
record_url_entry(url_string, body)
|
|
107
|
+
body
|
|
82
108
|
end
|
|
83
109
|
|
|
84
110
|
# @return [Rigor::Cache::Descriptor] frozen snapshot of every
|
|
85
|
-
# file the boundary has read so far. Calling this
|
|
86
|
-
# times yields equal descriptors; subsequent
|
|
87
|
-
# the underlying record
|
|
111
|
+
# file / URL the boundary has read so far. Calling this
|
|
112
|
+
# multiple times yields equal descriptors; subsequent
|
|
113
|
+
# reads expand the underlying record tables.
|
|
88
114
|
def cache_descriptor
|
|
89
|
-
|
|
90
|
-
Cache::Descriptor.new(files:
|
|
115
|
+
files, configs = @mutex.synchronize { [@file_entries.values.dup, @config_entries.values.dup] }
|
|
116
|
+
Cache::Descriptor.new(files: files, configs: configs)
|
|
91
117
|
end
|
|
92
118
|
|
|
93
119
|
private
|
|
@@ -97,6 +123,53 @@ module Rigor
|
|
|
97
123
|
entry = Cache::Descriptor::FileEntry.new(path: path, comparator: :digest, value: digest)
|
|
98
124
|
@mutex.synchronize { @file_entries[path] = entry }
|
|
99
125
|
end
|
|
126
|
+
|
|
127
|
+
def record_url_entry(url, body)
|
|
128
|
+
digest = Digest::SHA256.hexdigest(body)
|
|
129
|
+
key = "url:#{url}"
|
|
130
|
+
entry = Cache::Descriptor::ConfigEntry.new(key: key, value_hash: digest)
|
|
131
|
+
@mutex.synchronize { @config_entries[key] = entry }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Default HTTP client wrapping `Net::HTTP`. Wraps a single
|
|
136
|
+
# `GET` over HTTPS. Specs inject a fake client that conforms
|
|
137
|
+
# to the same `#get(url, timeout:, max_bytes:)` shape so the
|
|
138
|
+
# tests don't require network access.
|
|
139
|
+
class DefaultHttpClient
|
|
140
|
+
# rubocop:disable Metrics/MethodLength
|
|
141
|
+
def get(url, timeout:, max_bytes:)
|
|
142
|
+
require "net/http"
|
|
143
|
+
require "uri"
|
|
144
|
+
|
|
145
|
+
uri = URI.parse(url)
|
|
146
|
+
body = +""
|
|
147
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true,
|
|
148
|
+
open_timeout: timeout,
|
|
149
|
+
read_timeout: timeout) do |http|
|
|
150
|
+
http.request_get(uri.request_uri) do |response|
|
|
151
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
152
|
+
raise Plugin::AccessDeniedError.new(
|
|
153
|
+
"URL #{url.inspect} returned non-success status #{response.code}",
|
|
154
|
+
reason: :url_fetch_failed,
|
|
155
|
+
resource: url
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
response.read_body do |chunk|
|
|
159
|
+
body << chunk
|
|
160
|
+
if body.bytesize > max_bytes
|
|
161
|
+
raise Plugin::AccessDeniedError.new(
|
|
162
|
+
"URL #{url.inspect} body exceeds #{max_bytes} bytes",
|
|
163
|
+
reason: :url_body_too_large,
|
|
164
|
+
resource: url
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
body
|
|
171
|
+
end
|
|
172
|
+
# rubocop:enable Metrics/MethodLength
|
|
100
173
|
end
|
|
101
174
|
end
|
|
102
175
|
end
|
|
@@ -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/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/version.rb
CHANGED
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
|
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,9 +184,13 @@ 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
|
|
@@ -203,6 +207,8 @@ files:
|
|
|
203
207
|
- lib/rigor/cache/rbs_known_class_names.rb
|
|
204
208
|
- lib/rigor/cache/store.rb
|
|
205
209
|
- lib/rigor/cli.rb
|
|
210
|
+
- lib/rigor/cli/diff_command.rb
|
|
211
|
+
- lib/rigor/cli/explain_command.rb
|
|
206
212
|
- lib/rigor/cli/type_of_command.rb
|
|
207
213
|
- lib/rigor/cli/type_of_renderer.rb
|
|
208
214
|
- lib/rigor/cli/type_scan_command.rb
|