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
|
@@ -11,6 +11,8 @@ require_relative "../source/node_locator"
|
|
|
11
11
|
require_relative "../inference/fallback_tracer"
|
|
12
12
|
require_relative "../inference/scope_indexer"
|
|
13
13
|
require_relative "type_of_renderer"
|
|
14
|
+
require_relative "command"
|
|
15
|
+
require_relative "options"
|
|
14
16
|
|
|
15
17
|
module Rigor
|
|
16
18
|
class CLI
|
|
@@ -25,21 +27,15 @@ module Rigor
|
|
|
25
27
|
# dispatching and lets us evolve the type-of UX (extra flags, watch mode,
|
|
26
28
|
# streaming output) without bloating the CLI shell. Output formatting is
|
|
27
29
|
# delegated to {TypeOfRenderer}.
|
|
28
|
-
class TypeOfCommand
|
|
30
|
+
class TypeOfCommand < Command
|
|
29
31
|
USAGE = "Usage: rigor type-of [options] FILE:LINE:COL"
|
|
30
32
|
|
|
31
33
|
Result = Data.define(:file, :line, :column, :node, :type, :tracer)
|
|
32
34
|
|
|
33
|
-
def initialize(argv:, out:, err:)
|
|
34
|
-
@argv = argv
|
|
35
|
-
@out = out
|
|
36
|
-
@err = err
|
|
37
|
-
end
|
|
38
|
-
|
|
39
35
|
# @return [Integer] CLI exit status.
|
|
40
36
|
def run
|
|
41
37
|
options = parse_options
|
|
42
|
-
buffer = resolve_buffer_binding(options)
|
|
38
|
+
buffer = Options.resolve_buffer_binding(options, err: @err)
|
|
43
39
|
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
44
40
|
|
|
45
41
|
target = parse_position_argument(@argv)
|
|
@@ -58,42 +54,13 @@ module Rigor
|
|
|
58
54
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
59
55
|
opts.on("--trace", "Record fail-soft fallbacks via FallbackTracer") { options[:trace] = true }
|
|
60
56
|
opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
61
|
-
|
|
62
|
-
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
63
|
-
options[:tmp_file] = value
|
|
64
|
-
end
|
|
65
|
-
opts.on("--instead-of=PATH",
|
|
66
|
-
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
67
|
-
options[:instead_of] = value
|
|
68
|
-
end
|
|
57
|
+
Options.add_editor_mode(opts, options)
|
|
69
58
|
end
|
|
70
59
|
parser.parse!(@argv)
|
|
71
60
|
|
|
72
61
|
options
|
|
73
62
|
end
|
|
74
63
|
|
|
75
|
-
# Mirrors `Rigor::CLI#resolve_buffer_binding` (the `check`
|
|
76
|
-
# path). Returns nil / BufferBinding / :usage_error. The
|
|
77
|
-
# symbol return path lets the caller translate to
|
|
78
|
-
# `CLI::EXIT_USAGE` without raising.
|
|
79
|
-
def resolve_buffer_binding(options)
|
|
80
|
-
tmp = options[:tmp_file]
|
|
81
|
-
instead = options[:instead_of]
|
|
82
|
-
return nil if tmp.nil? && instead.nil?
|
|
83
|
-
|
|
84
|
-
if tmp.nil? || instead.nil?
|
|
85
|
-
@err.puts("--tmp-file and --instead-of must appear together")
|
|
86
|
-
return :usage_error
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
unless File.file?(tmp)
|
|
90
|
-
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
91
|
-
return :usage_error
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
Rigor::Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
64
|
def execute(target:, options:, buffer: nil)
|
|
98
65
|
file, line, column = target
|
|
99
66
|
# Under editor mode the logical `file` may not exist on disk
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "optionparser"
|
|
5
5
|
|
|
6
|
+
require_relative "renderable"
|
|
7
|
+
|
|
6
8
|
module Rigor
|
|
7
9
|
class CLI
|
|
8
10
|
# Renders a `TypeOfCommand::Result` as either human-readable text or a
|
|
@@ -12,19 +14,12 @@ module Rigor
|
|
|
12
14
|
# output formats (sexp, lsp-style hover payloads, color decoration) can
|
|
13
15
|
# plug in without disturbing argument parsing or the inference call site.
|
|
14
16
|
class TypeOfRenderer
|
|
17
|
+
include Renderable
|
|
18
|
+
|
|
15
19
|
def initialize(out:)
|
|
16
20
|
@out = out
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
def render(result, format:)
|
|
20
|
-
case format
|
|
21
|
-
when "text" then render_text(result)
|
|
22
|
-
when "json" then render_json(result)
|
|
23
|
-
else
|
|
24
|
-
raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
23
|
private
|
|
29
24
|
|
|
30
25
|
def render_text(result)
|
|
@@ -9,6 +9,7 @@ require_relative "../inference/coverage_scanner"
|
|
|
9
9
|
require_relative "../scope"
|
|
10
10
|
require_relative "type_scan_renderer"
|
|
11
11
|
require_relative "type_scan_report"
|
|
12
|
+
require_relative "command"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
class CLI
|
|
@@ -19,21 +20,15 @@ module Rigor
|
|
|
19
20
|
# the inference engine's directly recognized classes. It is the project's
|
|
20
21
|
# primary CI gate for tracking how much of an input source the engine can
|
|
21
22
|
# name without falling back to `Dynamic[Top]`.
|
|
22
|
-
class TypeScanCommand
|
|
23
|
+
class TypeScanCommand < Command
|
|
23
24
|
USAGE = "Usage: rigor type-scan [options] PATH..."
|
|
24
25
|
|
|
25
26
|
LocatedEvent = Data.define(:file, :event)
|
|
26
27
|
|
|
27
|
-
def initialize(argv:, out:, err:)
|
|
28
|
-
@argv = argv
|
|
29
|
-
@out = out
|
|
30
|
-
@err = err
|
|
31
|
-
end
|
|
32
|
-
|
|
33
28
|
# @return [Integer] CLI exit status.
|
|
34
29
|
def run
|
|
35
30
|
options = parse_options
|
|
36
|
-
paths = collect_paths(@argv)
|
|
31
|
+
paths = collect_paths(@argv, command_name: "type-scan")
|
|
37
32
|
return CLI::EXIT_USAGE if paths.nil?
|
|
38
33
|
return usage_error if paths.empty?
|
|
39
34
|
|
|
@@ -67,21 +62,6 @@ module Rigor
|
|
|
67
62
|
options
|
|
68
63
|
end
|
|
69
64
|
|
|
70
|
-
def collect_paths(args)
|
|
71
|
-
paths = []
|
|
72
|
-
args.each do |arg|
|
|
73
|
-
if File.directory?(arg)
|
|
74
|
-
paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
|
|
75
|
-
elsif File.file?(arg)
|
|
76
|
-
paths << arg
|
|
77
|
-
else
|
|
78
|
-
@err.puts("type-scan: not a file or directory: #{arg}")
|
|
79
|
-
return nil
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
paths.uniq
|
|
83
|
-
end
|
|
84
|
-
|
|
85
65
|
def usage_error
|
|
86
66
|
@err.puts("type-scan: at least one path is required")
|
|
87
67
|
@err.puts(USAGE)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "optionparser"
|
|
5
5
|
|
|
6
|
+
require_relative "renderable"
|
|
7
|
+
|
|
6
8
|
module Rigor
|
|
7
9
|
class CLI
|
|
8
10
|
# Renders a `TypeScanCommand::Report` as either a terminal-friendly text
|
|
@@ -11,19 +13,12 @@ module Rigor
|
|
|
11
13
|
# the two formats stay in lockstep; that pairing is why this class is a
|
|
12
14
|
# bit longer than the default class-length budget.
|
|
13
15
|
class TypeScanRenderer
|
|
16
|
+
include Renderable
|
|
17
|
+
|
|
14
18
|
def initialize(out:)
|
|
15
19
|
@out = out
|
|
16
20
|
end
|
|
17
21
|
|
|
18
|
-
def render(report, format:)
|
|
19
|
-
case format
|
|
20
|
-
when "text" then render_text(report)
|
|
21
|
-
when "json" then render_json(report)
|
|
22
|
-
else
|
|
23
|
-
raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
22
|
private
|
|
28
23
|
|
|
29
24
|
def render_text(report)
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "configuration"
|
|
|
9
9
|
require_relative "version"
|
|
10
10
|
require_relative "analysis/diagnostic"
|
|
11
11
|
require_relative "analysis/result"
|
|
12
|
+
require_relative "cli/options"
|
|
12
13
|
|
|
13
14
|
module Rigor
|
|
14
15
|
# The CLI class is a dispatcher: each `run_*` method delegates to a
|
|
@@ -77,16 +78,19 @@ module Rigor
|
|
|
77
78
|
EXIT_USAGE
|
|
78
79
|
end
|
|
79
80
|
|
|
80
|
-
def run_check
|
|
81
|
+
def run_check # rubocop:disable Metrics/AbcSize
|
|
81
82
|
load_check_dependencies
|
|
82
83
|
options = parse_check_options
|
|
83
|
-
buffer = resolve_buffer_binding(options)
|
|
84
|
+
buffer = Options.resolve_buffer_binding(options, err: @err)
|
|
84
85
|
return EXIT_USAGE if buffer == :usage_error
|
|
85
86
|
|
|
86
87
|
configuration = load_check_configuration(options)
|
|
87
88
|
cache_root = configuration.cache_path
|
|
88
89
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
89
90
|
|
|
91
|
+
special = dispatch_special_check_mode(configuration, options, cache_root)
|
|
92
|
+
return special unless special.nil?
|
|
93
|
+
|
|
90
94
|
runner = build_check_runner(
|
|
91
95
|
configuration: configuration, options: options,
|
|
92
96
|
buffer: buffer, cache_root: cache_root
|
|
@@ -97,6 +101,7 @@ module Rigor
|
|
|
97
101
|
write_result(result, options.fetch(:format))
|
|
98
102
|
write_run_stats(result.stats) if result.stats
|
|
99
103
|
write_trace_appendices
|
|
104
|
+
runner.cache_store&.evict!
|
|
100
105
|
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
101
106
|
|
|
102
107
|
exit_code = result.success? ? 0 : 1
|
|
@@ -104,6 +109,95 @@ module Rigor
|
|
|
104
109
|
exit_code
|
|
105
110
|
end
|
|
106
111
|
|
|
112
|
+
# ADR-46 — the two incremental-analysis check modes both fully handle
|
|
113
|
+
# the run and return an exit code (so `run_check` short-circuits);
|
|
114
|
+
# returns nil for an ordinary check.
|
|
115
|
+
def dispatch_special_check_mode(configuration, options, cache_root)
|
|
116
|
+
return run_verify_incremental(configuration) if options.fetch(:verify_incremental)
|
|
117
|
+
return run_incremental_check(configuration, options, cache_root) if options.fetch(:incremental)
|
|
118
|
+
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# ADR-46 — the incremental-analysis acceptance gate. Runs a baseline
|
|
123
|
+
# analysis (recording cross-file dependencies), then re-analyzes a
|
|
124
|
+
# representative subset of files and serves the rest from the per-file
|
|
125
|
+
# cache (the body tier), and asserts the merged diagnostics are
|
|
126
|
+
# byte-identical to a full `--no-cache` analysis. A mismatch means the
|
|
127
|
+
# incremental machinery would serve a stale — manufactured —
|
|
128
|
+
# diagnostic, the soundness failure this gate exists to catch. Prints a
|
|
129
|
+
# one-line PASS (exit 0) or the differing diagnostics (exit 1).
|
|
130
|
+
def run_verify_incremental(configuration)
|
|
131
|
+
paths = @argv.empty? ? nil : @argv
|
|
132
|
+
session = Analysis::IncrementalSession.new(configuration: configuration, paths: paths)
|
|
133
|
+
session.baseline
|
|
134
|
+
analyzed = session.analyzed_files
|
|
135
|
+
|
|
136
|
+
# Every other file forms the re-analyzed subset, so the run exercises
|
|
137
|
+
# BOTH the subset-analysis path and the cache-serving path.
|
|
138
|
+
subset = analyzed.each_with_index.select { |_, index| index.even? }.map(&:first)
|
|
139
|
+
incremental = normalize_diagnostics(session.reanalyze_subset(subset))
|
|
140
|
+
full = normalize_diagnostics(verify_full_diagnostics(configuration, paths))
|
|
141
|
+
|
|
142
|
+
report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# ADR-46 — cross-process incremental analysis (`--incremental`). Derives
|
|
146
|
+
# the global fingerprint cheaply (no RBS env build), loads the disk
|
|
147
|
+
# snapshot, and on a fingerprint hit re-analyzes only the files changed
|
|
148
|
+
# since the last run (plus their dependents), serving the rest from the
|
|
149
|
+
# snapshot; on a miss runs a full baseline. Persists the updated
|
|
150
|
+
# snapshot for the next invocation. Diagnostics are identical to a full
|
|
151
|
+
# run (the `--verify-incremental` gate enforces this); the win is
|
|
152
|
+
# skipping per-file inference for unchanged files.
|
|
153
|
+
def run_incremental_check(configuration, options, cache_root)
|
|
154
|
+
paths = @argv.empty? ? nil : @argv
|
|
155
|
+
probe = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
156
|
+
files = paths ? probe.analysis_file_set(paths) : probe.analysis_file_set
|
|
157
|
+
fingerprint = Cache::IncrementalSnapshot.fingerprint(
|
|
158
|
+
configuration: configuration, roots: paths || configuration.paths
|
|
159
|
+
)
|
|
160
|
+
snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
|
|
161
|
+
session = Analysis::IncrementalSession.new(configuration: configuration, paths: paths)
|
|
162
|
+
|
|
163
|
+
diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
|
|
164
|
+
@err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
|
|
165
|
+
"(#{files.size} files)")
|
|
166
|
+
|
|
167
|
+
result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration, options)
|
|
168
|
+
write_result(result, options.fetch(:format))
|
|
169
|
+
result.success? ? 0 : 1
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def verify_full_diagnostics(configuration, paths)
|
|
173
|
+
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
174
|
+
(paths ? runner.run(paths) : runner.run).diagnostics
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def normalize_diagnostics(diagnostics)
|
|
178
|
+
diagnostics.map(&:to_h).sort_by do |hash|
|
|
179
|
+
[hash["path"].to_s, hash["line"].to_i, hash["column"].to_i, hash["rule"].to_s, hash["message"].to_s]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def report_verify_incremental(incremental, full, subset_size:, total:)
|
|
184
|
+
if incremental == full
|
|
185
|
+
@out.puts("rigor: --verify-incremental OK — incremental " \
|
|
186
|
+
"(#{subset_size}/#{total} files re-analyzed, rest from cache) " \
|
|
187
|
+
"matches full (#{full.size} diagnostics)")
|
|
188
|
+
return 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
only_incremental = incremental - full
|
|
192
|
+
only_full = full - incremental
|
|
193
|
+
@err.puts("rigor: --verify-incremental FAILED — incremental and full diagnostics differ.")
|
|
194
|
+
@err.puts(" incremental-only: #{only_incremental.size}, full-only: #{only_full.size}")
|
|
195
|
+
(only_incremental + only_full).first(10).each do |hash|
|
|
196
|
+
@err.puts(" #{hash['path']}:#{hash['line']}:#{hash['column']}: [#{hash['rule']}] #{hash['message']}")
|
|
197
|
+
end
|
|
198
|
+
1
|
|
199
|
+
end
|
|
200
|
+
|
|
107
201
|
# ADR-22 slice 5 — the `--baseline-strict` CI gate. When the
|
|
108
202
|
# flag is set, ANY baseline drift fails the run — not only
|
|
109
203
|
# excess drift (a bucket over threshold, which already fails
|
|
@@ -190,7 +284,14 @@ module Rigor
|
|
|
190
284
|
end
|
|
191
285
|
|
|
192
286
|
def build_check_runner(configuration:, options:, buffer:, cache_root:)
|
|
193
|
-
cache_store = options.fetch(:no_cache)
|
|
287
|
+
cache_store = if options.fetch(:no_cache)
|
|
288
|
+
nil
|
|
289
|
+
else
|
|
290
|
+
Cache::Store.new(
|
|
291
|
+
root: cache_root,
|
|
292
|
+
max_bytes: configuration.cache_max_bytes
|
|
293
|
+
)
|
|
294
|
+
end
|
|
194
295
|
Analysis::Runner.new(
|
|
195
296
|
configuration: configuration,
|
|
196
297
|
explain: options.fetch(:explain),
|
|
@@ -201,37 +302,6 @@ module Rigor
|
|
|
201
302
|
)
|
|
202
303
|
end
|
|
203
304
|
|
|
204
|
-
# Editor-mode CLI envelope. The `--tmp-file=PATH` /
|
|
205
|
-
# `--instead-of=PATH` pair binds an in-flight buffer file to
|
|
206
|
-
# the logical project path it represents (see
|
|
207
|
-
# `docs/design/20260516-editor-mode.md`). Both flags must
|
|
208
|
-
# appear together; either alone is a usage error. The
|
|
209
|
-
# physical file must be readable; missing-file is a usage
|
|
210
|
-
# error too so editors get one consistent failure shape.
|
|
211
|
-
#
|
|
212
|
-
# Returns:
|
|
213
|
-
# - `nil` when neither flag was supplied (legacy path).
|
|
214
|
-
# - `Rigor::Analysis::BufferBinding` when the pair is valid.
|
|
215
|
-
# - `:usage_error` after writing one diagnostic to stderr;
|
|
216
|
-
# the caller MUST translate this to `EXIT_USAGE`.
|
|
217
|
-
def resolve_buffer_binding(options)
|
|
218
|
-
tmp = options[:tmp_file]
|
|
219
|
-
instead = options[:instead_of]
|
|
220
|
-
return nil if tmp.nil? && instead.nil?
|
|
221
|
-
|
|
222
|
-
if tmp.nil? || instead.nil?
|
|
223
|
-
@err.puts("--tmp-file and --instead-of must appear together")
|
|
224
|
-
return :usage_error
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
unless File.file?(tmp)
|
|
228
|
-
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
229
|
-
return :usage_error
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
305
|
# ADR-15 Phase 4c — resolves the worker count by
|
|
236
306
|
# precedence: CLI `--workers=N` (most explicit) > env
|
|
237
307
|
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
@@ -293,7 +363,18 @@ module Rigor
|
|
|
293
363
|
# `.rigor.yml`. Intended for single-file / ad-hoc CI use;
|
|
294
364
|
# ordinary projects should configure the plugin in
|
|
295
365
|
# `.rigor.yml`.
|
|
296
|
-
treat_all_as_inline_rbs: false
|
|
366
|
+
treat_all_as_inline_rbs: false,
|
|
367
|
+
# ADR-46 — the incremental-analysis acceptance gate. Runs a
|
|
368
|
+
# baseline analysis, re-analyzes a subset and serves the rest from
|
|
369
|
+
# the per-file cache, and asserts the merged diagnostics are
|
|
370
|
+
# byte-identical to a full `--no-cache` run. Exits non-zero on any
|
|
371
|
+
# mismatch. Off by default.
|
|
372
|
+
verify_incremental: false,
|
|
373
|
+
# ADR-46 — cross-process incremental analysis. With a disk snapshot
|
|
374
|
+
# of the prior run's per-file diagnostics + dependency graph,
|
|
375
|
+
# re-analyzes only the changed closure and serves the rest from the
|
|
376
|
+
# snapshot. Off by default.
|
|
377
|
+
incremental: false
|
|
297
378
|
}
|
|
298
379
|
parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
299
380
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
@@ -311,14 +392,7 @@ module Rigor
|
|
|
311
392
|
"Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
|
|
312
393
|
options[:workers] = value
|
|
313
394
|
end
|
|
314
|
-
|
|
315
|
-
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
316
|
-
options[:tmp_file] = value
|
|
317
|
-
end
|
|
318
|
-
opts.on("--instead-of=PATH",
|
|
319
|
-
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
320
|
-
options[:instead_of] = value
|
|
321
|
-
end
|
|
395
|
+
Options.add_editor_mode(opts, options)
|
|
322
396
|
opts.on("--baseline=PATH",
|
|
323
397
|
"ADR-22: load baseline from PATH (overrides .rigor.yml `baseline:`)") do |value|
|
|
324
398
|
options[:baseline] = value
|
|
@@ -335,6 +409,14 @@ module Rigor
|
|
|
335
409
|
"ADR-32: force-load rigor-rbs-inline with require_magic_comment: false") do
|
|
336
410
|
options[:treat_all_as_inline_rbs] = true
|
|
337
411
|
end
|
|
412
|
+
opts.on("--verify-incremental",
|
|
413
|
+
"ADR-46: assert incremental analysis matches a full run, then exit") do
|
|
414
|
+
options[:verify_incremental] = true
|
|
415
|
+
end
|
|
416
|
+
opts.on("--incremental",
|
|
417
|
+
"ADR-46: re-analyze only files changed since the last run (cross-process cache)") do
|
|
418
|
+
options[:incremental] = true
|
|
419
|
+
end
|
|
338
420
|
end
|
|
339
421
|
parser.parse!(@argv)
|
|
340
422
|
options
|
|
@@ -77,7 +77,7 @@ module Rigor
|
|
|
77
77
|
return new([]) if data.nil?
|
|
78
78
|
raise ArgumentError, "dependencies: must be a Hash, got #{data.inspect}" unless data.is_a?(Hash)
|
|
79
79
|
|
|
80
|
-
raw_entries =
|
|
80
|
+
raw_entries = coerce_source_inference(data["source_inference"])
|
|
81
81
|
entries, warnings = dedupe_entries(raw_entries)
|
|
82
82
|
budget = coerce_budget_per_gem(data.fetch("budget_per_gem", DEFAULT_BUDGET_PER_GEM))
|
|
83
83
|
strategy = coerce_budget_overrun_strategy(
|
|
@@ -158,6 +158,23 @@ module Rigor
|
|
|
158
158
|
|
|
159
159
|
private
|
|
160
160
|
|
|
161
|
+
# `source_inference:` is a list of per-gem entries, or `false` /
|
|
162
|
+
# omitted to disable it (the default). Guard the Ruby
|
|
163
|
+
# `Array(false) == [false]` quirk that would otherwise feed `false`
|
|
164
|
+
# straight into coerce_entry and crash the whole run on a perfectly
|
|
165
|
+
# reasonable "off" config (`dependencies: { source_inference: false }`).
|
|
166
|
+
def coerce_source_inference(value)
|
|
167
|
+
return [] if value.nil? || value == false
|
|
168
|
+
|
|
169
|
+
unless value.is_a?(Array)
|
|
170
|
+
raise ArgumentError,
|
|
171
|
+
"dependencies.source_inference: must be a list of entries " \
|
|
172
|
+
"(or false / omitted to disable), got #{value.inspect}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
value.map { |raw| coerce_entry(raw) }
|
|
176
|
+
end
|
|
177
|
+
|
|
161
178
|
def coerce_entry(raw)
|
|
162
179
|
unless raw.is_a?(Hash)
|
|
163
180
|
raise ArgumentError,
|
|
@@ -39,6 +39,7 @@ module Rigor
|
|
|
39
39
|
PROFILES = {
|
|
40
40
|
lenient: {
|
|
41
41
|
"call.undefined-method" => :error,
|
|
42
|
+
"call.self-undefined-method" => :off,
|
|
42
43
|
"call.unresolved-toplevel" => :off,
|
|
43
44
|
"call.wrong-arity" => :error,
|
|
44
45
|
"call.argument-type-mismatch" => :warning,
|
|
@@ -47,6 +48,7 @@ module Rigor
|
|
|
47
48
|
"flow.unreachable-branch" => :info,
|
|
48
49
|
"flow.dead-assignment" => :info,
|
|
49
50
|
"flow.always-truthy-condition" => :info,
|
|
51
|
+
"flow.unreachable-clause" => :info,
|
|
50
52
|
"assert.type-mismatch" => :error,
|
|
51
53
|
"dump.type" => :info,
|
|
52
54
|
"def.return-type-mismatch" => :warning,
|
|
@@ -54,10 +56,15 @@ module Rigor
|
|
|
54
56
|
"def.override-visibility-reduced" => :off,
|
|
55
57
|
"def.override-return-widened" => :off,
|
|
56
58
|
"def.override-param-narrowed" => :off,
|
|
57
|
-
"def.ivar-write-mismatch" => :warning
|
|
59
|
+
"def.ivar-write-mismatch" => :warning,
|
|
60
|
+
# Opt-in author assertion: you only see it if you wrote a
|
|
61
|
+
# `conforms-to` directive, so it stays a :warning even in
|
|
62
|
+
# lenient — it is never unsolicited noise.
|
|
63
|
+
"rbs_extended.unsatisfied-conformance" => :warning
|
|
58
64
|
}.freeze,
|
|
59
65
|
balanced: {
|
|
60
66
|
"call.undefined-method" => :error,
|
|
67
|
+
"call.self-undefined-method" => :off,
|
|
61
68
|
"call.unresolved-toplevel" => :warning,
|
|
62
69
|
"call.wrong-arity" => :error,
|
|
63
70
|
"call.argument-type-mismatch" => :error,
|
|
@@ -66,6 +73,11 @@ module Rigor
|
|
|
66
73
|
"flow.unreachable-branch" => :warning,
|
|
67
74
|
"flow.dead-assignment" => :warning,
|
|
68
75
|
"flow.always-truthy-condition" => :warning,
|
|
76
|
+
# ADR-47 WD4: stays :info (not :warning like its siblings) in the
|
|
77
|
+
# default balanced profile until the regression-corpus FP gate is
|
|
78
|
+
# green; promote to :warning once Mastodon/GitLab/Redmine triage
|
|
79
|
+
# to zero net false positives.
|
|
80
|
+
"flow.unreachable-clause" => :info,
|
|
69
81
|
"assert.type-mismatch" => :error,
|
|
70
82
|
"dump.type" => :info,
|
|
71
83
|
"def.return-type-mismatch" => :warning,
|
|
@@ -73,10 +85,12 @@ module Rigor
|
|
|
73
85
|
"def.override-visibility-reduced" => :warning,
|
|
74
86
|
"def.override-return-widened" => :warning,
|
|
75
87
|
"def.override-param-narrowed" => :warning,
|
|
76
|
-
"def.ivar-write-mismatch" => :warning
|
|
88
|
+
"def.ivar-write-mismatch" => :warning,
|
|
89
|
+
"rbs_extended.unsatisfied-conformance" => :warning
|
|
77
90
|
}.freeze,
|
|
78
91
|
strict: {
|
|
79
92
|
"call.undefined-method" => :error,
|
|
93
|
+
"call.self-undefined-method" => :off,
|
|
80
94
|
"call.unresolved-toplevel" => :error,
|
|
81
95
|
"call.wrong-arity" => :error,
|
|
82
96
|
"call.argument-type-mismatch" => :error,
|
|
@@ -85,6 +99,10 @@ module Rigor
|
|
|
85
99
|
"flow.unreachable-branch" => :error,
|
|
86
100
|
"flow.dead-assignment" => :error,
|
|
87
101
|
"flow.always-truthy-condition" => :error,
|
|
102
|
+
# ADR-47: strict opts into the new rule at :warning (one notch
|
|
103
|
+
# below its :error siblings) while it proves out — see the
|
|
104
|
+
# balanced-profile note above.
|
|
105
|
+
"flow.unreachable-clause" => :warning,
|
|
88
106
|
"assert.type-mismatch" => :error,
|
|
89
107
|
"dump.type" => :error,
|
|
90
108
|
"def.return-type-mismatch" => :error,
|
|
@@ -92,7 +110,8 @@ module Rigor
|
|
|
92
110
|
"def.override-visibility-reduced" => :error,
|
|
93
111
|
"def.override-return-widened" => :error,
|
|
94
112
|
"def.override-param-narrowed" => :error,
|
|
95
|
-
"def.ivar-write-mismatch" => :error
|
|
113
|
+
"def.ivar-write-mismatch" => :error,
|
|
114
|
+
"rbs_extended.unsatisfied-conformance" => :error
|
|
96
115
|
}.freeze
|
|
97
116
|
}.freeze
|
|
98
117
|
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -69,7 +69,13 @@ module Rigor
|
|
|
69
69
|
"baseline" => nil,
|
|
70
70
|
"fold_platform_specific_paths" => false,
|
|
71
71
|
"cache" => {
|
|
72
|
-
"path" => ".rigor/cache"
|
|
72
|
+
"path" => ".rigor/cache",
|
|
73
|
+
# LRU eviction cap in bytes. nil (the default) disables eviction;
|
|
74
|
+
# the cache grows until the user runs `rigor check --clear-cache`.
|
|
75
|
+
# Set to a positive integer (e.g. 536870912 for 512 MB) to keep the
|
|
76
|
+
# cache bounded — the least-recently-used entries are removed at the
|
|
77
|
+
# end of each run when the total exceeds this limit.
|
|
78
|
+
"max_bytes" => nil
|
|
73
79
|
},
|
|
74
80
|
"plugins_io" => {
|
|
75
81
|
"network" => "disabled",
|
|
@@ -166,7 +172,8 @@ module Rigor
|
|
|
166
172
|
PATH_KEYS = %w[paths signature_paths pre_eval].freeze
|
|
167
173
|
private_constant :PATH_KEYS
|
|
168
174
|
|
|
169
|
-
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :
|
|
175
|
+
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :cache_max_bytes,
|
|
176
|
+
:disabled_rules,
|
|
170
177
|
:libraries, :signature_paths, :fold_platform_specific_paths,
|
|
171
178
|
:plugins_io_network, :plugins_io_allowed_paths,
|
|
172
179
|
:plugins_io_allowed_url_hosts,
|
|
@@ -334,6 +341,8 @@ module Rigor
|
|
|
334
341
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
335
342
|
) == true
|
|
336
343
|
@cache_path = cache.fetch("path").to_s
|
|
344
|
+
raw_max = cache.fetch("max_bytes")
|
|
345
|
+
@cache_max_bytes = raw_max.nil? ? nil : Integer(raw_max)
|
|
337
346
|
@plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
|
|
338
347
|
@plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
|
|
339
348
|
@plugins_io_allowed_url_hosts = Array(plugins_io.fetch("allowed_url_hosts")).map(&:to_s).freeze
|
|
@@ -383,7 +392,8 @@ module Rigor
|
|
|
383
392
|
"pre_eval" => pre_eval,
|
|
384
393
|
"fold_platform_specific_paths" => fold_platform_specific_paths,
|
|
385
394
|
"cache" => {
|
|
386
|
-
"path" => cache_path
|
|
395
|
+
"path" => cache_path,
|
|
396
|
+
"max_bytes" => cache_max_bytes
|
|
387
397
|
},
|
|
388
398
|
"plugins_io" => {
|
|
389
399
|
"network" => plugins_io_network.to_s,
|