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.
@@ -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)` — always raises {AccessDeniedError} while
31
- # `network_policy` is `:disabled` (the only setting in slice
32
- # 2). The hook exists so slices 3-6 can layer richer access
33
- # policy without re-defining the API.
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
- # Slice 2 stub: every URL access is denied while
68
- # `network_policy` is `:disabled`. Slices that need to relax
69
- # the rule (e.g. for opt-in offline-replay caches) will lift
70
- # the policy gate; the API does not change.
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
- unless @policy.network_allowed?
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
- "network access is disabled during analysis",
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: url.to_s
101
+ resource: url_string
78
102
  )
79
103
  end
80
104
 
81
- raise NotImplementedError, "URL fetch surface is reserved; slice 2 only ships the deny path"
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 multiple
86
- # times yields equal descriptors; subsequent reads expand
87
- # the underlying record table.
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
- entries = @mutex.synchronize { @file_entries.values.dup }
90
- Cache::Descriptor.new(files: entries)
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`: `:disabled` in slice 2; the only value
36
- # accepted today. Plugin {IoBoundary#open_url} always raises
37
- # while the policy is `:disabled`.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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.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