rigortype 0.1.16 → 0.1.17
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/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
- data/lib/rigor/analysis/check_rules.rb +149 -70
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner.rb +434 -37
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +147 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +99 -1
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +125 -43
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +13 -3
- data/lib/rigor/environment/rbs_loader.rb +76 -3
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +140 -20
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +99 -59
- data/lib/rigor/inference/narrowing.rb +202 -5
- data/lib/rigor/inference/scope_indexer.rb +134 -7
- data/lib/rigor/inference/statement_evaluator.rb +105 -26
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/base.rb +20 -4
- data/lib/rigor/plugin/registry.rb +39 -1
- data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope.rb +123 -9
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +17 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/inference.rbs +22 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +5 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- metadata +22 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "rbs_descriptor"
|
|
4
|
+
require_relative "rbs_cache_producer"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Cache
|
|
@@ -18,19 +19,12 @@ module Rigor
|
|
|
18
19
|
# Cache descriptor shape is shared with {RbsConstantTable} via
|
|
19
20
|
# {RbsDescriptor.build}; a single signature change or rbs gem
|
|
20
21
|
# bump invalidates both producers in lockstep.
|
|
21
|
-
class RbsKnownClassNames
|
|
22
|
+
class RbsKnownClassNames < RbsCacheProducer
|
|
22
23
|
PRODUCER_ID = "rbs.known_class_names"
|
|
23
24
|
|
|
24
25
|
# @param loader [Rigor::Environment::RbsLoader]
|
|
25
26
|
# @param store [Rigor::Cache::Store]
|
|
26
27
|
# @return [Set<String>]
|
|
27
|
-
def self.fetch(loader:, store:)
|
|
28
|
-
descriptor = RbsDescriptor.build(loader)
|
|
29
|
-
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
30
|
-
compute(loader)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
28
|
def self.compute(loader)
|
|
35
29
|
names = Set.new
|
|
36
30
|
loader.each_known_class_name { |name| names << name }
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -44,9 +44,10 @@ module Rigor
|
|
|
44
44
|
# invocations can read from the same cache concurrently
|
|
45
45
|
# without churning it. See
|
|
46
46
|
# `docs/design/20260516-editor-mode.md` § "Cache behaviour".
|
|
47
|
-
def initialize(root:, read_only: false)
|
|
47
|
+
def initialize(root:, read_only: false, max_bytes: nil)
|
|
48
48
|
@root = root.to_s.dup.freeze
|
|
49
49
|
@read_only = read_only
|
|
50
|
+
@max_bytes = max_bytes&.then { |n| Integer(n) }
|
|
50
51
|
@hits = 0
|
|
51
52
|
@misses = 0
|
|
52
53
|
@writes = 0
|
|
@@ -197,6 +198,81 @@ module Rigor
|
|
|
197
198
|
value
|
|
198
199
|
end
|
|
199
200
|
|
|
201
|
+
# ADR-45 — record-and-validate variant. Unlike {fetch_or_compute},
|
|
202
|
+
# which keys the entry on its descriptor (so the inputs MUST be
|
|
203
|
+
# known before running), this keys on `key_descriptor` (the stable
|
|
204
|
+
# inputs known up front) and stores, alongside the value, a
|
|
205
|
+
# `dependency_descriptor` of the files the value actually read —
|
|
206
|
+
# including inputs discovered DURING the computation (e.g. a plugin
|
|
207
|
+
# reading a file mid-analysis). On the next run the stored
|
|
208
|
+
# dependencies are re-validated against the filesystem
|
|
209
|
+
# ({Descriptor#fresh?}); a stale dependency forces a recompute.
|
|
210
|
+
#
|
|
211
|
+
# The block MUST return `[value, dependency_descriptor]`. Disk reads
|
|
212
|
+
# are not in-process-memoised — validation always re-checks the
|
|
213
|
+
# filesystem — but a single run only looks up once.
|
|
214
|
+
def fetch_or_validate(producer_id:, key_descriptor:, params: {}, serialize: nil, deserialize: nil)
|
|
215
|
+
validate_producer_id!(producer_id)
|
|
216
|
+
ensure_schema_version!
|
|
217
|
+
|
|
218
|
+
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
219
|
+
path = entry_path(producer_id, key)
|
|
220
|
+
cached = read_entry(path, deserialize: deserialize)
|
|
221
|
+
if cached && (pair = cached.value).is_a?(Array) && pair.size == 2 &&
|
|
222
|
+
pair[1].is_a?(Descriptor) && pair[1].fresh?
|
|
223
|
+
@monitor.synchronize { record(:hits, producer_id) }
|
|
224
|
+
return pair[0]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
|
|
228
|
+
wrote = false
|
|
229
|
+
unless @read_only
|
|
230
|
+
# A cache write must never break the run. If the value is not
|
|
231
|
+
# Marshal-clean (or any disk error occurs) skip caching and
|
|
232
|
+
# return the freshly-computed value — the next run recomputes.
|
|
233
|
+
begin
|
|
234
|
+
write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
|
|
235
|
+
wrote = true
|
|
236
|
+
rescue StandardError
|
|
237
|
+
wrote = false
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
@monitor.synchronize do
|
|
241
|
+
record(:misses, producer_id)
|
|
242
|
+
record(:writes, producer_id) if wrote
|
|
243
|
+
end
|
|
244
|
+
value
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# ADR-6 § "Eviction" — LRU pass over the on-disk cache. No-op when
|
|
248
|
+
# `max_bytes:` was not configured or the store is read-only.
|
|
249
|
+
# Walks all `.entry` files, sorts by mtime ascending (oldest = least
|
|
250
|
+
# recently used), and unlinks from the oldest until the total is at
|
|
251
|
+
# or below the cap. Touch-on-disk-read ({read_entry}) is the
|
|
252
|
+
# cross-process LRU signal: every disk hit (not in-process-memo hit)
|
|
253
|
+
# updates the mtime so recently-read entries survive the eviction pass.
|
|
254
|
+
# Any FS error is swallowed — eviction must never break a run.
|
|
255
|
+
def evict!
|
|
256
|
+
return if @max_bytes.nil? || @read_only
|
|
257
|
+
|
|
258
|
+
entries = collect_entry_stats
|
|
259
|
+
total = entries.sum { |e| e[:bytes] }
|
|
260
|
+
return if total <= @max_bytes
|
|
261
|
+
|
|
262
|
+
entries.sort_by! { |e| e[:mtime] }
|
|
263
|
+
entries.each do |entry|
|
|
264
|
+
break if total <= @max_bytes
|
|
265
|
+
|
|
266
|
+
File.unlink(entry[:path])
|
|
267
|
+
total -= entry[:bytes]
|
|
268
|
+
rescue StandardError
|
|
269
|
+
next
|
|
270
|
+
end
|
|
271
|
+
nil
|
|
272
|
+
rescue StandardError
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
200
276
|
private
|
|
201
277
|
|
|
202
278
|
Entry = Data.define(:descriptor_bytes, :value)
|
|
@@ -238,6 +314,7 @@ module Rigor
|
|
|
238
314
|
value = safe_load(value_bytes, deserialize)
|
|
239
315
|
return nil if value.equal?(LOAD_FAILED)
|
|
240
316
|
|
|
317
|
+
touch_for_lru(path) if @max_bytes
|
|
241
318
|
Entry.new(descriptor_bytes, value)
|
|
242
319
|
end
|
|
243
320
|
|
|
@@ -370,6 +447,27 @@ module Rigor
|
|
|
370
447
|
end
|
|
371
448
|
end
|
|
372
449
|
|
|
450
|
+
# Updates both atime and mtime of `path` to the current time — the
|
|
451
|
+
# cross-process LRU signal used by {evict!}. Best-effort: any FS
|
|
452
|
+
# error (read-only mount, deleted file) is silently ignored.
|
|
453
|
+
def touch_for_lru(path)
|
|
454
|
+
now = Time.now
|
|
455
|
+
File.utime(now, now, path)
|
|
456
|
+
rescue StandardError
|
|
457
|
+
nil
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Returns an array of `{ path:, mtime:, bytes: }` hashes for every
|
|
461
|
+
# `.entry` file under the cache root, skipping unreadable entries.
|
|
462
|
+
def collect_entry_stats
|
|
463
|
+
Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
|
|
464
|
+
stat = File.stat(path)
|
|
465
|
+
{ path: path, mtime: stat.mtime, bytes: stat.size }
|
|
466
|
+
rescue StandardError
|
|
467
|
+
nil
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
373
471
|
def read_varint(bytes, offset)
|
|
374
472
|
result = 0
|
|
375
473
|
shift = 0
|
|
@@ -9,6 +9,7 @@ require_relative "../scope"
|
|
|
9
9
|
require_relative "../inference/def_return_typer"
|
|
10
10
|
require_relative "../inference/scope_indexer"
|
|
11
11
|
require_relative "prism_colorizer"
|
|
12
|
+
require_relative "command"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
class CLI
|
|
@@ -25,19 +26,13 @@ module Rigor
|
|
|
25
26
|
# since the appended text is always a comment — and printed to
|
|
26
27
|
# stdout with IRB-style syntax highlighting via
|
|
27
28
|
# {PrismColorizer}.
|
|
28
|
-
class AnnotateCommand
|
|
29
|
+
class AnnotateCommand < Command
|
|
29
30
|
USAGE = "Usage: rigor annotate [options] FILE"
|
|
30
31
|
|
|
31
32
|
# Appended ` #=> dump_type: <type>` suffix. Matched and
|
|
32
33
|
# stripped before re-annotating so re-running is idempotent.
|
|
33
34
|
ANNOTATION_PATTERN = /\s*#=>\s*dump_type:.*\z/
|
|
34
35
|
|
|
35
|
-
def initialize(argv:, out:, err:)
|
|
36
|
-
@argv = argv
|
|
37
|
-
@out = out
|
|
38
|
-
@err = err
|
|
39
|
-
end
|
|
40
|
-
|
|
41
36
|
# @return [Integer] CLI exit status.
|
|
42
37
|
def run
|
|
43
38
|
options = parse_options
|
|
@@ -6,6 +6,7 @@ require_relative "../analysis/baseline"
|
|
|
6
6
|
require_relative "../analysis/runner"
|
|
7
7
|
require_relative "../cache/store"
|
|
8
8
|
require_relative "../configuration"
|
|
9
|
+
require_relative "command"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
class CLI
|
|
@@ -20,18 +21,12 @@ module Rigor
|
|
|
20
21
|
# rigor baseline dump
|
|
21
22
|
# rigor baseline drift
|
|
22
23
|
# rigor baseline prune
|
|
23
|
-
class BaselineCommand # rubocop:disable Metrics/ClassLength
|
|
24
|
+
class BaselineCommand < Command # rubocop:disable Metrics/ClassLength
|
|
24
25
|
EXIT_USAGE = 64
|
|
25
26
|
DEFAULT_BASELINE_PATH = ".rigor-baseline.yml"
|
|
26
27
|
|
|
27
28
|
SUBCOMMANDS = %w[generate regenerate dump drift prune].freeze
|
|
28
29
|
|
|
29
|
-
def initialize(argv:, out: $stdout, err: $stderr)
|
|
30
|
-
@argv = argv
|
|
31
|
-
@out = out
|
|
32
|
-
@err = err
|
|
33
|
-
end
|
|
34
|
-
|
|
35
30
|
def run
|
|
36
31
|
subcommand = @argv.shift
|
|
37
32
|
case subcommand
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# Base class for the `rigor <subcommand>` command objects.
|
|
6
|
+
#
|
|
7
|
+
# Every subcommand captured the same invariant wiring — the argument
|
|
8
|
+
# vector plus the output and error streams — in an identical
|
|
9
|
+
# `initialize(argv:, out:, err:)`, and defaulted the streams
|
|
10
|
+
# inconsistently (some to `$stdout` / `$stderr`, most not at all).
|
|
11
|
+
# Centralising it here gives one consistent shape and lets a test
|
|
12
|
+
# instantiate a command with just `argv:` (the streams default so a
|
|
13
|
+
# spec can pass a `StringIO` for one and ignore the other).
|
|
14
|
+
#
|
|
15
|
+
# Subclasses read the `@argv` / `@out` / `@err` ivars directly, as
|
|
16
|
+
# they did before.
|
|
17
|
+
class Command
|
|
18
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
19
|
+
@argv = argv
|
|
20
|
+
@out = out
|
|
21
|
+
@err = err
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Expands `args` (a mix of files and directories) into a unique
|
|
27
|
+
# list of `.rb` paths, recursing into directories. Returns nil —
|
|
28
|
+
# after writing `<command_name>: not a file or directory: <arg>` to
|
|
29
|
+
# `@err` — on the first arg that is neither. Shared by the
|
|
30
|
+
# path-walking commands (`type-scan`, `coverage`).
|
|
31
|
+
def collect_paths(args, command_name:)
|
|
32
|
+
paths = []
|
|
33
|
+
args.each do |arg|
|
|
34
|
+
if File.directory?(arg)
|
|
35
|
+
paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
|
|
36
|
+
elsif File.file?(arg)
|
|
37
|
+
paths << arg
|
|
38
|
+
else
|
|
39
|
+
@err.puts("#{command_name}: not a file or directory: #{arg}")
|
|
40
|
+
return nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
paths.uniq
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -9,6 +9,7 @@ require_relative "../inference/precision_scanner"
|
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "coverage_report"
|
|
11
11
|
require_relative "coverage_renderer"
|
|
12
|
+
require_relative "command"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
class CLI
|
|
@@ -25,19 +26,13 @@ module Rigor
|
|
|
25
26
|
# 0 — scan complete, precision ratio ≥ threshold (or no threshold given)
|
|
26
27
|
# 1 — precision ratio < threshold, or parse errors encountered
|
|
27
28
|
# 64 — usage error
|
|
28
|
-
class CoverageCommand
|
|
29
|
+
class CoverageCommand < Command
|
|
29
30
|
USAGE = "Usage: rigor coverage [options] PATH..."
|
|
30
31
|
|
|
31
|
-
def initialize(argv:, out:, err:)
|
|
32
|
-
@argv = argv
|
|
33
|
-
@out = out
|
|
34
|
-
@err = err
|
|
35
|
-
end
|
|
36
|
-
|
|
37
32
|
# @return [Integer] CLI exit status.
|
|
38
33
|
def run
|
|
39
34
|
options = parse_options
|
|
40
|
-
paths = collect_paths(@argv)
|
|
35
|
+
paths = collect_paths(@argv, command_name: "coverage")
|
|
41
36
|
return CLI::EXIT_USAGE if paths.nil?
|
|
42
37
|
return usage_error if paths.empty?
|
|
43
38
|
|
|
@@ -64,21 +59,6 @@ module Rigor
|
|
|
64
59
|
options
|
|
65
60
|
end
|
|
66
61
|
|
|
67
|
-
def collect_paths(args)
|
|
68
|
-
paths = []
|
|
69
|
-
args.each do |arg|
|
|
70
|
-
if File.directory?(arg)
|
|
71
|
-
paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
|
|
72
|
-
elsif File.file?(arg)
|
|
73
|
-
paths << arg
|
|
74
|
-
else
|
|
75
|
-
@err.puts("coverage: not a file or directory: #{arg}")
|
|
76
|
-
return nil
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
paths.uniq
|
|
80
|
-
end
|
|
81
|
-
|
|
82
62
|
def usage_error
|
|
83
63
|
@err.puts("coverage: at least one path is required")
|
|
84
64
|
@err.puts(USAGE)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "renderable"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
7
8
|
# Renders a `CoverageReport` as terminal-friendly text or JSON.
|
|
8
9
|
class CoverageRenderer
|
|
10
|
+
include Renderable
|
|
11
|
+
|
|
9
12
|
TIER_LABELS = {
|
|
10
13
|
constant: "constant",
|
|
11
14
|
nominal: "nominal",
|
|
@@ -21,14 +24,6 @@ module Rigor
|
|
|
21
24
|
@out = out
|
|
22
25
|
end
|
|
23
26
|
|
|
24
|
-
def render(report, format:)
|
|
25
|
-
case format
|
|
26
|
-
when "text" then render_text(report)
|
|
27
|
-
when "json" then render_json(report)
|
|
28
|
-
else raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
27
|
private
|
|
33
28
|
|
|
34
29
|
def render_text(report)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "json"
|
|
4
6
|
require "optionparser"
|
|
5
7
|
|
|
@@ -29,15 +31,9 @@ module Rigor
|
|
|
29
31
|
# is `1` when any new diagnostic appears, `0` otherwise —
|
|
30
32
|
# so adding new errors fails CI but legacy errors recorded
|
|
31
33
|
# in the baseline don't.
|
|
32
|
-
class DiffCommand
|
|
34
|
+
class DiffCommand < Command
|
|
33
35
|
USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
|
|
34
36
|
|
|
35
|
-
def initialize(argv:, out:, err:)
|
|
36
|
-
@argv = argv
|
|
37
|
-
@out = out
|
|
38
|
-
@err = err
|
|
39
|
-
end
|
|
40
|
-
|
|
41
37
|
# @return [Integer] CLI exit status.
|
|
42
38
|
def run
|
|
43
39
|
options = parse_options
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "optionparser"
|
|
5
5
|
|
|
6
6
|
require_relative "../analysis/rule_catalog"
|
|
7
|
+
require_relative "command"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
class CLI
|
|
@@ -17,15 +18,9 @@ module Rigor
|
|
|
17
18
|
# beyond the rendered catalog. Useful when a user sees a
|
|
18
19
|
# diagnostic in the editor and wants to know what the rule
|
|
19
20
|
# means without leaving the terminal.
|
|
20
|
-
class ExplainCommand
|
|
21
|
+
class ExplainCommand < Command
|
|
21
22
|
USAGE = "Usage: rigor explain [options] [<rule>]"
|
|
22
23
|
|
|
23
|
-
def initialize(argv:, out:, err:)
|
|
24
|
-
@argv = argv
|
|
25
|
-
@out = out
|
|
26
|
-
@err = err
|
|
27
|
-
end
|
|
28
|
-
|
|
29
24
|
# @return [Integer] CLI exit status.
|
|
30
25
|
def run
|
|
31
26
|
options = parse_options
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "optionparser"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -11,15 +13,9 @@ module Rigor
|
|
|
11
13
|
# The actual stdio JSON-RPC reader / writer is queued for slice 2;
|
|
12
14
|
# invoking `rigor lsp` at slice 1 returns immediately after
|
|
13
15
|
# validating the transport flag.
|
|
14
|
-
class LspCommand
|
|
16
|
+
class LspCommand < Command
|
|
15
17
|
USAGE = "Usage: rigor lsp [options]"
|
|
16
18
|
|
|
17
|
-
def initialize(argv:, out:, err:)
|
|
18
|
-
@argv = argv
|
|
19
|
-
@out = out
|
|
20
|
-
@err = err
|
|
21
|
-
end
|
|
22
|
-
|
|
23
19
|
# @return [Integer] CLI exit status.
|
|
24
20
|
def run
|
|
25
21
|
options = parse_options
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "optionparser"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -13,15 +15,9 @@ module Rigor
|
|
|
13
15
|
# Slice 1 ships the stdio transport with seven read-only tools:
|
|
14
16
|
# rigor_check, rigor_type_of, rigor_triage, rigor_annotate,
|
|
15
17
|
# rigor_sig_gen, rigor_explain, rigor_coverage.
|
|
16
|
-
class McpCommand
|
|
18
|
+
class McpCommand < Command
|
|
17
19
|
USAGE = "Usage: rigor mcp [options]"
|
|
18
20
|
|
|
19
|
-
def initialize(argv:, out:, err:)
|
|
20
|
-
@argv = argv
|
|
21
|
-
@out = out
|
|
22
|
-
@err = err
|
|
23
|
-
end
|
|
24
|
-
|
|
25
21
|
# @return [Integer] CLI exit status.
|
|
26
22
|
def run
|
|
27
23
|
options = parse_options
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analysis/buffer_binding"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Shared option plumbing for the subcommands.
|
|
8
|
+
#
|
|
9
|
+
# Today it owns the editor-mode surface that `rigor check` and
|
|
10
|
+
# `rigor type-of` both expose: the `--tmp-file` / `--instead-of` flag
|
|
11
|
+
# pair and the buffer-binding resolution that validates it. That
|
|
12
|
+
# resolution used to be copied verbatim into both `Rigor::CLI` and
|
|
13
|
+
# `TypeOfCommand`, so a fix to one (the paired-flag check, the
|
|
14
|
+
# readability check, the error wording) could silently miss the
|
|
15
|
+
# other. Centralising it keeps the two editor-mode entry points in
|
|
16
|
+
# lockstep.
|
|
17
|
+
module Options
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Defines the `--tmp-file` / `--instead-of` editor-mode flag pair
|
|
21
|
+
# on `parser`, writing into `options`.
|
|
22
|
+
def add_editor_mode(parser, options)
|
|
23
|
+
parser.on("--tmp-file=PATH",
|
|
24
|
+
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
25
|
+
options[:tmp_file] = value
|
|
26
|
+
end
|
|
27
|
+
parser.on("--instead-of=PATH",
|
|
28
|
+
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
29
|
+
options[:instead_of] = value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Resolves the editor-mode buffer binding from parsed `options`:
|
|
34
|
+
# returns nil when neither editor-mode flag is set, a
|
|
35
|
+
# `Analysis::BufferBinding` when the pair is valid, or
|
|
36
|
+
# `:usage_error` (after writing the reason to `err`) when the flags
|
|
37
|
+
# are unpaired or the temp file is unreadable.
|
|
38
|
+
def resolve_buffer_binding(options, err:)
|
|
39
|
+
tmp = options[:tmp_file]
|
|
40
|
+
instead = options[:instead_of]
|
|
41
|
+
return nil if tmp.nil? && instead.nil?
|
|
42
|
+
|
|
43
|
+
if tmp.nil? || instead.nil?
|
|
44
|
+
err.puts("--tmp-file and --instead-of must appear together")
|
|
45
|
+
return :usage_error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless File.file?(tmp)
|
|
49
|
+
err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
50
|
+
return :usage_error
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class CLI
|
|
5
7
|
# `rigor plugin` (singular) — discover and read the plugin source
|
|
@@ -44,7 +46,7 @@ module Rigor
|
|
|
44
46
|
# will not resolve — read them from the same environment that ran
|
|
45
47
|
# the command (`rigor plugin print` inlines the body for exactly
|
|
46
48
|
# this case: it works with no file-reading tool at all).
|
|
47
|
-
class PluginCommand
|
|
49
|
+
class PluginCommand < Command
|
|
48
50
|
USAGE = <<~USAGE
|
|
49
51
|
Usage: rigor plugin <subcommand> [args]
|
|
50
52
|
|
|
@@ -72,12 +74,6 @@ module Rigor
|
|
|
72
74
|
PLUGINS_ROOT = File.join(GEM_ROOT, "plugins")
|
|
73
75
|
EXAMPLES_ROOT = File.join(GEM_ROOT, "examples")
|
|
74
76
|
|
|
75
|
-
def initialize(argv:, out: $stdout, err: $stderr)
|
|
76
|
-
@argv = argv
|
|
77
|
-
@out = out
|
|
78
|
-
@err = err
|
|
79
|
-
end
|
|
80
|
-
|
|
81
77
|
# @return [Integer] CLI exit status.
|
|
82
78
|
def run
|
|
83
79
|
subcommand = @argv.shift || "list"
|
|
@@ -9,6 +9,7 @@ require_relative "../plugin/services"
|
|
|
9
9
|
require_relative "../reflection"
|
|
10
10
|
require_relative "../type/combinator"
|
|
11
11
|
require_relative "plugins_renderer"
|
|
12
|
+
require_relative "command"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
class CLI
|
|
@@ -60,15 +61,9 @@ module Rigor
|
|
|
60
61
|
# the RBS environment without conflict (requires constructing
|
|
61
62
|
# the Environment, which is heavier than the loader-only
|
|
62
63
|
# pass this slice does).
|
|
63
|
-
class PluginsCommand # rubocop:disable Metrics/ClassLength
|
|
64
|
+
class PluginsCommand < Command # rubocop:disable Metrics/ClassLength
|
|
64
65
|
USAGE = "Usage: rigor plugins [options]"
|
|
65
66
|
|
|
66
|
-
def initialize(argv:, out: $stdout, err: $stderr)
|
|
67
|
-
@argv = argv
|
|
68
|
-
@out = out
|
|
69
|
-
@err = err
|
|
70
|
-
end
|
|
71
|
-
|
|
72
67
|
# @return [Integer] CLI exit status.
|
|
73
68
|
def run
|
|
74
69
|
options = parse_options
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Output-format dispatch shared by the `--format text|json` renderers.
|
|
8
|
+
#
|
|
9
|
+
# Each renderer included this and then implemented `render_text` /
|
|
10
|
+
# `render_json`; the `render(data, format:)` entry point — route by
|
|
11
|
+
# the format string, raise one consistent `OptionParser::InvalidArgument`
|
|
12
|
+
# on anything else — was copied verbatim into every one. Centralising
|
|
13
|
+
# it keeps the unsupported-format wording and the text/json contract
|
|
14
|
+
# in a single place as new renderers and formats are added.
|
|
15
|
+
module Renderable
|
|
16
|
+
def render(data, format:)
|
|
17
|
+
case format
|
|
18
|
+
when "text" then render_text(data)
|
|
19
|
+
when "json" then render_json(data)
|
|
20
|
+
else
|
|
21
|
+
raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -4,6 +4,7 @@ require "optionparser"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
6
|
require_relative "../sig_gen"
|
|
7
|
+
require_relative "command"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
class CLI
|
|
@@ -34,19 +35,13 @@ module Rigor
|
|
|
34
35
|
# `--params=observed-strict` stays reserved-but-inert until
|
|
35
36
|
# the capability-role catalog ships (rejected with a usage
|
|
36
37
|
# error so the surface stays stable).
|
|
37
|
-
class SigGenCommand
|
|
38
|
+
class SigGenCommand < Command
|
|
38
39
|
USAGE = "Usage: rigor sig-gen [options] [paths]"
|
|
39
40
|
|
|
40
41
|
VALID_MODES = %w[print diff write].freeze
|
|
41
42
|
VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
|
|
42
43
|
VALID_FORMATS = %w[text json].freeze
|
|
43
44
|
|
|
44
|
-
def initialize(argv:, out:, err:)
|
|
45
|
-
@argv = argv
|
|
46
|
-
@out = out
|
|
47
|
-
@err = err
|
|
48
|
-
end
|
|
49
|
-
|
|
50
45
|
# @return [Integer] CLI exit status.
|
|
51
46
|
def run
|
|
52
47
|
options = parse_options
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "command"
|
|
4
|
+
|
|
3
5
|
require "optparse"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -29,7 +31,7 @@ module Rigor
|
|
|
29
31
|
# as input to a Read tool.
|
|
30
32
|
#
|
|
31
33
|
# `rigor skill` with no subcommand is an alias for `list`.
|
|
32
|
-
class SkillCommand
|
|
34
|
+
class SkillCommand < Command
|
|
33
35
|
USAGE = <<~USAGE
|
|
34
36
|
Usage: rigor skill <subcommand> [args]
|
|
35
37
|
|
|
@@ -48,12 +50,6 @@ module Rigor
|
|
|
48
50
|
# `lib/rigor/cli/skill_command.rb` that is three directories up.
|
|
49
51
|
SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
|
|
50
52
|
|
|
51
|
-
def initialize(argv:, out: $stdout, err: $stderr)
|
|
52
|
-
@argv = argv
|
|
53
|
-
@out = out
|
|
54
|
-
@err = err
|
|
55
|
-
end
|
|
56
|
-
|
|
57
53
|
# @return [Integer] CLI exit status.
|
|
58
54
|
def run
|
|
59
55
|
subcommand = @argv.shift || "list"
|
|
@@ -7,6 +7,7 @@ require_relative "../analysis/runner"
|
|
|
7
7
|
require_relative "../cache/store"
|
|
8
8
|
require_relative "../triage"
|
|
9
9
|
require_relative "triage_renderer"
|
|
10
|
+
require_relative "command"
|
|
10
11
|
|
|
11
12
|
module Rigor
|
|
12
13
|
class CLI
|
|
@@ -18,16 +19,10 @@ module Rigor
|
|
|
18
19
|
# Read-only and advisory (WD4): never edits config, never
|
|
19
20
|
# writes a baseline. Always exits 0 — it is an inspection
|
|
20
21
|
# command, not a gate (`rigor check` remains the gate).
|
|
21
|
-
class TriageCommand
|
|
22
|
+
class TriageCommand < Command
|
|
22
23
|
USAGE = "Usage: rigor triage [options] [paths]"
|
|
23
24
|
DEFAULT_SECTIONS = %i[distribution hotspots hints].freeze
|
|
24
25
|
|
|
25
|
-
def initialize(argv:, out:, err:)
|
|
26
|
-
@argv = argv
|
|
27
|
-
@out = out
|
|
28
|
-
@err = err
|
|
29
|
-
end
|
|
30
|
-
|
|
31
26
|
# @return [Integer] CLI exit status (always 0).
|
|
32
27
|
def run
|
|
33
28
|
options = parse_options
|