rigortype 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "options"
|
|
4
5
|
|
|
5
6
|
require "json"
|
|
6
7
|
require "optionparser"
|
|
@@ -63,7 +64,7 @@ module Rigor
|
|
|
63
64
|
options[:current_path] = path
|
|
64
65
|
end
|
|
65
66
|
opt.on("--config=PATH", "Path to .rigor.yml. Forwarded to the implicit `rigor check` run.") do |path|
|
|
66
|
-
options[:config] = path
|
|
67
|
+
options[:config] = Options.config_path(path)
|
|
67
68
|
end
|
|
68
69
|
end.parse!(@argv)
|
|
69
70
|
options
|
|
@@ -22,11 +22,6 @@ module Rigor
|
|
|
22
22
|
return CLI::EXIT_USAGE if options.nil?
|
|
23
23
|
|
|
24
24
|
config = options.fetch(:config)
|
|
25
|
-
if config && !File.file?(config)
|
|
26
|
-
usage_error("no configuration file at #{config}")
|
|
27
|
-
return CLI::EXIT_USAGE
|
|
28
|
-
end
|
|
29
|
-
|
|
30
25
|
configuration = Configuration.load(config)
|
|
31
26
|
config_path = options.fetch(:config) || Configuration.discover
|
|
32
27
|
# The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
|
|
@@ -56,6 +51,11 @@ module Rigor
|
|
|
56
51
|
options
|
|
57
52
|
rescue OptionParser::ParseError => e
|
|
58
53
|
usage_error(e.message)
|
|
54
|
+
rescue Rigor::ConfigurationError => e
|
|
55
|
+
# The shared --config check (CLI::Options.config_path). Mapped here the way Rigor::CLI#run maps it, so a
|
|
56
|
+
# direct LensCommand run exits with the usage code too.
|
|
57
|
+
@err.puts("rigor: #{e.message}")
|
|
58
|
+
nil
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def usage_error(message)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "options"
|
|
4
5
|
require_relative "../runtime/jit"
|
|
5
6
|
|
|
6
7
|
require "optionparser"
|
|
@@ -54,7 +55,7 @@ module Rigor
|
|
|
54
55
|
end
|
|
55
56
|
opts.on("--config=PATH",
|
|
56
57
|
"Session-level default config path (individual tool calls may override)") do |value|
|
|
57
|
-
options[:config] = value
|
|
58
|
+
options[:config] = Options.config_path(value)
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
parser.parse!(@argv)
|
data/lib/rigor/cli/options.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../analysis/buffer_binding"
|
|
4
|
+
require_relative "../configuration_error"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -10,15 +11,31 @@ module Rigor
|
|
|
10
11
|
# `--instead-of` flag pair and the buffer-binding resolution that validates it. That resolution used to be copied
|
|
11
12
|
# verbatim into both `Rigor::CLI` and `TypeOfCommand`, so a fix to one (the paired-flag check, the readability
|
|
12
13
|
# check, the error wording) could silently miss the other. Centralising it keeps the two editor-mode entry points in
|
|
13
|
-
# lockstep.
|
|
14
|
+
# lockstep. It also owns the `--config` check (#1992), so every command and the MCP tools that pass `--config`
|
|
15
|
+
# read the same rule.
|
|
14
16
|
module Options
|
|
15
17
|
module_function
|
|
16
18
|
|
|
17
19
|
# Defines the standard `--config=PATH` flag on `parser`, writing the path into `options[:config]`. Used by every
|
|
18
20
|
# subcommand that loads a `.rigor.yml`; the few whose `--config` help text is intentionally bespoke (`diff`,
|
|
19
|
-
# `mcp`, `show-bleedingedge`) keep their own `parser.on`
|
|
21
|
+
# `mcp`, `show-bleedingedge`) keep their own `parser.on` and call {.config_path} for the same check.
|
|
20
22
|
def add_config(parser, options)
|
|
21
|
-
parser.on("--config=PATH", "Path to the Rigor configuration file")
|
|
23
|
+
parser.on("--config=PATH", "Path to the Rigor configuration file") do |value|
|
|
24
|
+
options[:config] = config_path(value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The `--config` value, checked where the flag is read (#1992). A named file that is not there is an error, not
|
|
29
|
+
# a request for the defaults: the run would otherwise analyse under a configuration the caller did not ask for,
|
|
30
|
+
# and an MCP caller would get a result for it with no sign anything was missing. Auto-discovery, with no
|
|
31
|
+
# `--config` given, is not affected.
|
|
32
|
+
def config_path(value)
|
|
33
|
+
raise ConfigurationError, "empty config path" if value.strip.empty?
|
|
34
|
+
raise ConfigurationError, "config path is a directory: #{value}" if File.directory?(value)
|
|
35
|
+
raise ConfigurationError, "config file not found: #{value}" unless File.exist?(value)
|
|
36
|
+
raise ConfigurationError, "config file not readable: #{value}" unless File.readable?(value)
|
|
37
|
+
|
|
38
|
+
value
|
|
22
39
|
end
|
|
23
40
|
|
|
24
41
|
# Defines the `--tmp-file` / `--instead-of` editor-mode flag pair on `parser`, writing into `options`.
|
|
@@ -233,6 +233,8 @@ module Rigor
|
|
|
233
233
|
signature_paths: signature_path_rows(plugin),
|
|
234
234
|
open_receivers: manifest.open_receivers.dup,
|
|
235
235
|
owns_receivers: manifest.owns_receivers.dup,
|
|
236
|
+
per_invocation_classes: manifest.per_invocation_classes.dup,
|
|
237
|
+
per_invocation_non_action_macros: manifest.per_invocation_non_action_macros.map(&:to_s),
|
|
236
238
|
produces: manifest.produces.map(&:to_s),
|
|
237
239
|
consumes: manifest.consumes.map { |c| "#{c.plugin_id}/#{c.name}#{'?' if c.optional}" },
|
|
238
240
|
source_rbs_synthesizer: !manifest.source_rbs_synthesizer.nil?
|
|
@@ -265,6 +267,8 @@ module Rigor
|
|
|
265
267
|
signature_paths: [],
|
|
266
268
|
open_receivers: [],
|
|
267
269
|
owns_receivers: [],
|
|
270
|
+
per_invocation_classes: [],
|
|
271
|
+
per_invocation_non_action_macros: [],
|
|
268
272
|
produces: [],
|
|
269
273
|
consumes: [],
|
|
270
274
|
block_as_methods: 0,
|
|
@@ -345,6 +349,7 @@ module Rigor
|
|
|
345
349
|
config: {},
|
|
346
350
|
signature_paths: [],
|
|
347
351
|
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
352
|
+
per_invocation_classes: [], per_invocation_non_action_macros: [],
|
|
348
353
|
block_as_methods: 0, heredoc_templates: 0, trait_registries: 0,
|
|
349
354
|
type_node_resolvers: 0,
|
|
350
355
|
hkt_registrations: 0, hkt_definitions: 0,
|
|
@@ -185,12 +185,26 @@ module Rigor
|
|
|
185
185
|
lines = []
|
|
186
186
|
lines << " open_receivers: #{row[:open_receivers].join(', ')}" if row[:open_receivers].any?
|
|
187
187
|
lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
|
|
188
|
+
lines.concat(per_invocation_lines(row))
|
|
188
189
|
lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
|
|
189
190
|
lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
|
|
190
191
|
lines.concat(narrow_protocol_lines(row))
|
|
191
192
|
lines
|
|
192
193
|
end
|
|
193
194
|
|
|
195
|
+
# ADR-125 — the per-invocation classes a plugin vouches for, and the macros whose arguments name a method that is
|
|
196
|
+
# not one of their actions.
|
|
197
|
+
def per_invocation_lines(row)
|
|
198
|
+
lines = []
|
|
199
|
+
if row[:per_invocation_classes].any?
|
|
200
|
+
lines << " per_invocation_classes: #{row[:per_invocation_classes].join(', ')}"
|
|
201
|
+
end
|
|
202
|
+
if row[:per_invocation_non_action_macros].any?
|
|
203
|
+
lines << " per_invocation_non_action_macros: #{row[:per_invocation_non_action_macros].join(', ')}"
|
|
204
|
+
end
|
|
205
|
+
lines
|
|
206
|
+
end
|
|
207
|
+
|
|
194
208
|
# ADR-37 narrow extension protocols (node_rule / dynamic_return / narrowing_facts). Surfaced in the full report
|
|
195
209
|
# alongside the declarative surfaces; `--capabilities` is the focused view.
|
|
196
210
|
def narrow_protocol_lines(row)
|
|
@@ -249,8 +263,6 @@ module Rigor
|
|
|
249
263
|
"signature_paths" => row[:signature_paths].map do |sp|
|
|
250
264
|
{ "path" => sp[:path], "exists" => sp[:exists], "rbs_files" => sp[:rbs_files] }
|
|
251
265
|
end,
|
|
252
|
-
"open_receivers" => row[:open_receivers],
|
|
253
|
-
"owns_receivers" => row[:owns_receivers],
|
|
254
266
|
"produces" => row[:produces],
|
|
255
267
|
"consumes" => row[:consumes],
|
|
256
268
|
"block_as_methods" => row[:block_as_methods],
|
|
@@ -265,6 +277,16 @@ module Rigor
|
|
|
265
277
|
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
266
278
|
"narrowing_facts_methods" => row[:narrowing_facts_methods],
|
|
267
279
|
"load_error" => row[:load_error]
|
|
280
|
+
}.merge(receiver_json(row))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ADR-125 — the per-invocation keys are additive; an empty array when the plugin declares none.
|
|
284
|
+
def receiver_json(row)
|
|
285
|
+
{
|
|
286
|
+
"open_receivers" => row[:open_receivers],
|
|
287
|
+
"owns_receivers" => row[:owns_receivers],
|
|
288
|
+
"per_invocation_classes" => row[:per_invocation_classes],
|
|
289
|
+
"per_invocation_non_action_macros" => row[:per_invocation_non_action_macros]
|
|
268
290
|
}
|
|
269
291
|
end
|
|
270
292
|
|
|
@@ -6,6 +6,7 @@ require "optionparser"
|
|
|
6
6
|
require_relative "../bleeding_edge"
|
|
7
7
|
require_relative "../configuration"
|
|
8
8
|
require_relative "command"
|
|
9
|
+
require_relative "options"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
class CLI
|
|
@@ -44,7 +45,7 @@ module Rigor
|
|
|
44
45
|
options[:format] = fmt
|
|
45
46
|
end
|
|
46
47
|
opt.on("--config=PATH", "Path to a .rigor.yml (default: auto-discovery).") do |path|
|
|
47
|
-
options[:config] = path
|
|
48
|
+
options[:config] = Options.config_path(path)
|
|
48
49
|
end
|
|
49
50
|
end.parse!(@argv)
|
|
50
51
|
options
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "../analysis/path_expansion"
|
|
6
7
|
require_relative "../effects/config_envelopes"
|
|
7
8
|
require_relative "../effects/registry"
|
|
8
9
|
require_relative "options"
|
|
@@ -55,8 +56,11 @@ module Rigor
|
|
|
55
56
|
configuration = Configuration.load(options.fetch(:config))
|
|
56
57
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
57
58
|
|
|
59
|
+
path_errors = path_diagnostics(paths, configuration)
|
|
60
|
+
report_path_errors(path_errors, options)
|
|
61
|
+
|
|
58
62
|
observation_roots = observation_roots(configuration, options)
|
|
59
|
-
observations = collect_observations(configuration, options, observation_roots)
|
|
63
|
+
observations = collect_observations(configuration, options, observation_roots, paths)
|
|
60
64
|
generator = SigGen::Generator.new(configuration: configuration, paths: paths,
|
|
61
65
|
observations: observations,
|
|
62
66
|
include_private: options.fetch(:include_private),
|
|
@@ -69,15 +73,20 @@ module Rigor
|
|
|
69
73
|
when :write then dispatch_write(candidates, configuration, options)
|
|
70
74
|
when :check then dispatch_check(candidates, configuration, options)
|
|
71
75
|
else
|
|
72
|
-
dispatch_print_or_diff(candidates, mode, options, observation_roots)
|
|
76
|
+
dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
|
|
73
77
|
0
|
|
74
78
|
end
|
|
79
|
+
status = 1 if path_errors.any?(&:error?)
|
|
80
|
+
report_run_notes(candidates, generator, options)
|
|
81
|
+
status
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def report_run_notes(candidates, generator, options)
|
|
75
85
|
report_skipped(candidates, options)
|
|
76
86
|
report_inline_declared(candidates, options)
|
|
77
87
|
report_withheld_annotations(candidates, options)
|
|
78
88
|
report_unrenderable(generator.unrenderable)
|
|
79
89
|
report_unresolvable_superclasses(generator.unresolvable_superclasses)
|
|
80
|
-
status
|
|
81
90
|
end
|
|
82
91
|
|
|
83
92
|
private
|
|
@@ -220,13 +229,38 @@ module Rigor
|
|
|
220
229
|
unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
|
|
221
230
|
end
|
|
222
231
|
|
|
223
|
-
|
|
232
|
+
# #1991 — the path errors `rigor check` reports for the arguments: a path that is neither a directory nor a
|
|
233
|
+
# `.rb` file. Warned about beside files that will be read, an error when nothing will be, as `check` grades it.
|
|
234
|
+
def path_diagnostics(paths, configuration)
|
|
235
|
+
bad = Analysis::PathExpansion.unusable(paths)
|
|
236
|
+
return [] if bad.empty?
|
|
237
|
+
|
|
238
|
+
any_files = !Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns,
|
|
239
|
+
base: configuration.exclude_base).empty?
|
|
240
|
+
Analysis::PathExpansion.diagnostics(bad, any_files: any_files)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Without this a missing path was read as empty, so the run said "No candidates" and exited 0 (#1991). JSON
|
|
244
|
+
# print/diff carries the errors in its payload (see {#path_errors_in_payload?}); every other form says them on
|
|
245
|
+
# stderr, as `rigor check` does.
|
|
246
|
+
def report_path_errors(path_errors, options)
|
|
247
|
+
return if path_errors_in_payload?(options)
|
|
248
|
+
|
|
249
|
+
path_errors.each { |diagnostic| @err.puts(diagnostic.to_s) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def path_errors_in_payload?(options)
|
|
253
|
+
options.fetch(:format) == "json" && %w[print diff].include?(options.fetch(:mode))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
|
|
224
257
|
SigGen::Renderer.new(out: @out).render(
|
|
225
258
|
candidates: candidates,
|
|
226
259
|
mode: mode,
|
|
227
260
|
format: options.fetch(:format),
|
|
228
261
|
selection: options.fetch(:selection),
|
|
229
|
-
observation: observation_report(observation_roots, options)
|
|
262
|
+
observation: observation_report(observation_roots, options),
|
|
263
|
+
diagnostics: path_errors_in_payload?(options) ? path_errors : []
|
|
230
264
|
)
|
|
231
265
|
end
|
|
232
266
|
|
|
@@ -291,7 +325,11 @@ module Rigor
|
|
|
291
325
|
# specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
|
|
292
326
|
# root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
|
|
293
327
|
# naming the reason.
|
|
294
|
-
|
|
328
|
+
#
|
|
329
|
+
# The collector pre-indexes the classes of the paths the run generates for as well as the configured ones, so
|
|
330
|
+
# a call site on a class named only on the command line (`sig-gen ../lib` from a subdirectory, where no
|
|
331
|
+
# config file is discovered and the default `paths:` resolve under the subdirectory) attaches (#2078).
|
|
332
|
+
def collect_observations(configuration, options, observe_paths, paths)
|
|
295
333
|
return {} if options.fetch(:params) != "observed"
|
|
296
334
|
|
|
297
335
|
if observe_paths.empty?
|
|
@@ -299,7 +337,8 @@ module Rigor
|
|
|
299
337
|
else
|
|
300
338
|
warn_missing_test_roots(observe_paths)
|
|
301
339
|
end
|
|
302
|
-
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths
|
|
340
|
+
SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths,
|
|
341
|
+
source_paths: (configuration.paths + paths).uniq).collect
|
|
303
342
|
end
|
|
304
343
|
|
|
305
344
|
# The roots `--params=observed` reads: `--observe=PATH...` when given, else the project's test roots.
|
|
@@ -17,6 +17,7 @@ require_relative "type_of_template_probe"
|
|
|
17
17
|
require_relative "command"
|
|
18
18
|
require_relative "options"
|
|
19
19
|
require_relative "../project_environment"
|
|
20
|
+
require_relative "type_of_project_scope"
|
|
20
21
|
|
|
21
22
|
module Rigor
|
|
22
23
|
class CLI
|
|
@@ -88,7 +89,7 @@ module Rigor
|
|
|
88
89
|
def execute(targets:, options:, buffer: nil)
|
|
89
90
|
configuration = Configuration.load(options.fetch(:config))
|
|
90
91
|
environment = project_environment(targets.map(&:file).uniq, configuration, buffer)
|
|
91
|
-
base_scope =
|
|
92
|
+
base_scope = TypeOfProjectScope.build(environment: environment, configuration: configuration, buffer: buffer)
|
|
92
93
|
|
|
93
94
|
results_by_target = Array.new(targets.length)
|
|
94
95
|
targets.each_with_index.group_by { |target, _index| target.file }.each do |file, indexed_targets|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../scope"
|
|
4
|
+
require_relative "../protection/discovery_seed"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
class CLI
|
|
8
|
+
# Issue #512 — the scope `rigor type-of` probes with: single-file, plus the cross-file tables a prior
|
|
9
|
+
# `rigor check --incremental` left in the snapshot ({Protection::DiscoverySeed.from_snapshot}) when there are
|
|
10
|
+
# any. A probe never re-parses the project for its seed, so without a reusable snapshot this is the bare
|
|
11
|
+
# `Scope.empty` it always was.
|
|
12
|
+
#
|
|
13
|
+
# The seed is the full {Inference::ProjectSeedTables} fold, so a `contested_*` sibling travels with the member
|
|
14
|
+
# it qualifies (ADR-119): this is a recorded whole-index consumer in `admission_census.yml`.
|
|
15
|
+
module TypeOfProjectScope
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def build(environment:, configuration:, buffer: nil)
|
|
19
|
+
base = Scope.empty(environment: environment)
|
|
20
|
+
seed = Protection::DiscoverySeed.from_snapshot(configuration: configuration, buffer: buffer)
|
|
21
|
+
return base if seed.nil? || seed.empty?
|
|
22
|
+
|
|
23
|
+
base.with_discovery(base.discovery.with(**seed))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -156,9 +156,8 @@ module Rigor
|
|
|
156
156
|
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
157
157
|
disable_token_warning(token.to_s, plugin_ids)
|
|
158
158
|
end
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
end
|
|
159
|
+
keys = configuration.severity_overrides.keys.map(&:to_s)
|
|
160
|
+
overrides = keys.filter_map { |key| override_key_warning(key, plugin_ids, keys) }
|
|
162
161
|
disable + overrides
|
|
163
162
|
end
|
|
164
163
|
|
|
@@ -172,14 +171,26 @@ module Rigor
|
|
|
172
171
|
# `plugin.`-qualified key naming an unconfigured plugin, and any key outside the built-in vocabulary that is
|
|
173
172
|
# not a qualified plugin id (a plugin's bare rule included), is inert. A reserved run-level row is the home of
|
|
174
173
|
# this map, so naming one is never flagged; `all` is not a family wildcard here, so it is.
|
|
175
|
-
def self.override_key_warning(key, plugin_ids)
|
|
176
|
-
reason = override_key_problem(key, plugin_ids)
|
|
174
|
+
def self.override_key_warning(key, plugin_ids, present = [])
|
|
175
|
+
reason = legacy_alias_problem(key, present) || override_key_problem(key, plugin_ids)
|
|
177
176
|
return nil if reason.nil?
|
|
178
177
|
|
|
179
178
|
Warning.new(kind: :severity_override, message: "severity_overrides: #{key.inspect} #{reason}",
|
|
180
179
|
fields: { "token" => key })
|
|
181
180
|
end
|
|
182
181
|
|
|
182
|
+
# #2005 — `disable:` expands a legacy alias to its canonical id, but the severity resolver matches canonical ids
|
|
183
|
+
# only, so an alias key reaches no row. Name the canonical id rather than accept the key. `present` holds the
|
|
184
|
+
# keys the map already has: when the canonical id is one of them, the advice to write it is already followed.
|
|
185
|
+
def self.legacy_alias_problem(key, present)
|
|
186
|
+
canonical = Analysis::CheckRules::LEGACY_RULE_ALIASES[key]
|
|
187
|
+
return nil if canonical.nil?
|
|
188
|
+
|
|
189
|
+
reason = "is a legacy alias of `#{canonical}`; the override has no effect"
|
|
190
|
+
present.include?(canonical) ? reason : "#{reason}, so write `#{canonical}`"
|
|
191
|
+
end
|
|
192
|
+
private_class_method :legacy_alias_problem
|
|
193
|
+
|
|
183
194
|
def self.override_key_problem(key, plugin_ids)
|
|
184
195
|
return "is not a recognized rule id; the override has no effect" if key == "all"
|
|
185
196
|
return nil unless Array(Analysis::CheckRules.resolve_rule_token(key)).empty?
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -321,12 +321,22 @@ module Rigor
|
|
|
321
321
|
|
|
322
322
|
# The test roots a run uses: the declared `test_paths:` as written (already resolved against the
|
|
323
323
|
# config file's directory by {.load}), or — when the key is unset — whichever of
|
|
324
|
-
# {CONVENTIONAL_TEST_PATHS} is a directory under
|
|
325
|
-
# the
|
|
326
|
-
|
|
324
|
+
# {CONVENTIONAL_TEST_PATHS} is a directory under a root, as absolute paths. The roots, unless `root` names
|
|
325
|
+
# one, are the directory of the config file in use, the one `paths:` and a declared `test_paths:` resolve
|
|
326
|
+
# against, and then the working directory, read only when the first holds neither. Auto-detecting against
|
|
327
|
+
# the working directory alone found nothing for a run from a subdirectory, or for the MCP server started
|
|
328
|
+
# elsewhere, though both name the config file (#2078); a config file kept in a subdirectory
|
|
329
|
+
# (`--config=config/rigor.yml`) still finds the project's `spec/` from the project root.
|
|
330
|
+
def resolved_test_paths(root: nil)
|
|
327
331
|
return test_paths unless test_paths.nil?
|
|
328
332
|
|
|
329
|
-
|
|
333
|
+
roots = root ? [root] : [exclude_base, Dir.pwd].uniq
|
|
334
|
+
roots.each do |dir|
|
|
335
|
+
found = CONVENTIONAL_TEST_PATHS.map { |test_dir| File.absolute_path(test_dir, dir) }
|
|
336
|
+
.select { File.directory?(it) }
|
|
337
|
+
return found unless found.empty?
|
|
338
|
+
end
|
|
339
|
+
[]
|
|
330
340
|
end
|
|
331
341
|
|
|
332
342
|
# Loads a configuration file.
|
|
@@ -10,6 +10,7 @@ require_relative "member_consistency"
|
|
|
10
10
|
require_relative "required_features"
|
|
11
11
|
require_relative "gated_signature_guard"
|
|
12
12
|
require_relative "gated_signature_plan"
|
|
13
|
+
require_relative "ruby_library_roots"
|
|
13
14
|
require_relative "../rooted_glob"
|
|
14
15
|
|
|
15
16
|
module Rigor
|
|
@@ -1653,9 +1654,10 @@ module Rigor
|
|
|
1653
1654
|
# arity. Read back by the env-cache producer, which rebuilds the env from the loader's readers on
|
|
1654
1655
|
# a miss — a list it cannot read is a stand-down that runs only on a `--no-cache` build.
|
|
1655
1656
|
def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
|
|
1656
|
-
deferred_signature_paths: [])
|
|
1657
|
+
deferred_signature_paths: [], project_signature_paths: [], required_features: [])
|
|
1657
1658
|
@libraries = libraries.map(&:to_s).freeze
|
|
1658
1659
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
1660
|
+
assign_reopen_inputs(project_signature_paths, required_features)
|
|
1659
1661
|
# Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and
|
|
1660
1662
|
# allowed to stand down against a colliding generic arity ({.add_deferred_signatures}). Every entry
|
|
1661
1663
|
# is also in `@signature_paths`, so the env cache descriptor already digests these files.
|
|
@@ -1764,6 +1766,9 @@ module Rigor
|
|
|
1764
1766
|
# a partial sidecar as authoritative made `sig-gen --write` on redmine report 5.9x more
|
|
1765
1767
|
# `call.undefined-method` than the same project with no `sig/` at all.
|
|
1766
1768
|
#
|
|
1769
|
+
# A class declared only by RBS synthesized from the project's own source joins the set too
|
|
1770
|
+
# ({#inline_only_entry?}, issue #1934).
|
|
1771
|
+
#
|
|
1767
1772
|
# Memoised per loader: one pass over `class_decls` (~1-2k entries), and every consumer is a check
|
|
1768
1773
|
# rule on the analysis hot path. Attribution is by buffer NAME, which survives the ADR-54 environment
|
|
1769
1774
|
# cache since #725 — an environment blob written before that carries the `<cached>` sentinel instead,
|
|
@@ -1777,10 +1782,14 @@ module Rigor
|
|
|
1777
1782
|
return Set.new.freeze if environment.nil?
|
|
1778
1783
|
|
|
1779
1784
|
project_files = self.class.project_sig_files(@signature_paths)
|
|
1780
|
-
|
|
1785
|
+
virtual_names = @virtual_rbs.to_set(&:first)
|
|
1786
|
+
return Set.new.freeze if project_files.empty? && virtual_names.empty?
|
|
1781
1787
|
|
|
1782
1788
|
names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
|
|
1783
|
-
|
|
1789
|
+
next unless self.class.project_entry?(entry, project_files) ||
|
|
1790
|
+
inline_only_entry?(entry, virtual_names, project_files)
|
|
1791
|
+
|
|
1792
|
+
acc << rbs_name.to_s.delete_prefix("::")
|
|
1784
1793
|
end
|
|
1785
1794
|
names.freeze
|
|
1786
1795
|
rescue ::RBS::BaseError
|
|
@@ -1788,6 +1797,92 @@ module Rigor
|
|
|
1788
1797
|
end
|
|
1789
1798
|
private :build_project_declared_classes
|
|
1790
1799
|
|
|
1800
|
+
# Issue #1934 — a class whose every declaration a source RBS synthesizer (`rigor-rbs-inline`) wrote from
|
|
1801
|
+
# the project's own `.rb` files is the project's as much as a `sig/` sidecar is: the declaration is the
|
|
1802
|
+
# inline comments of the source under analysis, so a method another project file adds to the class (a
|
|
1803
|
+
# plain `def`, or an `alias_method` whose target the inline declaration names) is the second file of an
|
|
1804
|
+
# ordinary class, not an ADR-17 monkey-patch. Reading the synthesized RBS as bundled reported
|
|
1805
|
+
# `alias_method :title, :label` from another file while the `alias` keyword, which upstream translates,
|
|
1806
|
+
# read clean.
|
|
1807
|
+
#
|
|
1808
|
+
# EVERY declaration, not only the primary one, and only virtual or `sig/` buffers: a core, stdlib or gem
|
|
1809
|
+
# class an inline-annotated file reopens (`class String` with a `#:` method) keeps its bundled
|
|
1810
|
+
# declaration, and a project `def` on it stays the monkey-patch ADR-17 reports. A declaration whose
|
|
1811
|
+
# buffer cannot be attributed (the pre-#725 `<cached>` sentinel) fails the test, which keeps the
|
|
1812
|
+
# pre-#1934 reading.
|
|
1813
|
+
def inline_only_entry?(entry, virtual_names, project_files)
|
|
1814
|
+
return false if virtual_names.empty?
|
|
1815
|
+
|
|
1816
|
+
project_authored_entry?(entry, virtual_names, project_files)
|
|
1817
|
+
end
|
|
1818
|
+
private :inline_only_entry?
|
|
1819
|
+
|
|
1820
|
+
# Every declaration of `entry` sits in a project `sig/` file or in RBS synthesized from the project's
|
|
1821
|
+
# source; a declaration whose buffer cannot be attributed fails the test.
|
|
1822
|
+
def project_authored_entry?(entry, virtual_names, project_files)
|
|
1823
|
+
buffers = self.class.entry_declarations(entry).map { |decl| self.class.declaration_buffer_name(decl) }
|
|
1824
|
+
return false if buffers.empty? || buffers.any?(&:nil?)
|
|
1825
|
+
|
|
1826
|
+
buffers.all? { |name| virtual_names.include?(name) || project_files.include?(File.absolute_path(name)) }
|
|
1827
|
+
end
|
|
1828
|
+
private :project_authored_entry?
|
|
1829
|
+
|
|
1830
|
+
# Issue #2174 — true when every RBS declaration of `class_name` is the project's own (rbs-inline `#:`
|
|
1831
|
+
# annotations, or a file under the configured `signature_paths:`) and its root constant belongs to a library
|
|
1832
|
+
# Ruby ships ({RubyLibraryRoots}) that the project's source requires ({RequiredFeatures.features_of}). Such
|
|
1833
|
+
# a declaration reopens a library class whose real signatures did not load (`class Matrix` with one
|
|
1834
|
+
# annotated method beside `require "matrix"`, and no `matrix` RBS), so it lists only the methods the
|
|
1835
|
+
# project added: the class's surface is not enumerable from it, and a call it omits is not evidence of an
|
|
1836
|
+
# undefined method. A library whose RBS did load contributes a declaration of its own (core, stdlib, a gem's
|
|
1837
|
+
# `sig/`, an rbs collection, a plugin's `sig/`), which takes the class out of this predicate. A project
|
|
1838
|
+
# class that shares a required library's root name declines too; that is the accepted cost.
|
|
1839
|
+
def ruby_library_reopen?(class_name)
|
|
1840
|
+
name = class_name.to_s.delete_prefix("::")
|
|
1841
|
+
return false if @required_features.empty? || !project_authored_classes.include?(name)
|
|
1842
|
+
|
|
1843
|
+
segments = ruby_library_roots[name.split("::").first]
|
|
1844
|
+
!segments.nil? && @required_features.any? { |feature| segments.include?(feature.split("/").first) }
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
def ruby_library_roots
|
|
1848
|
+
@state[:ruby_library_roots] ||= RubyLibraryRoots.shared
|
|
1849
|
+
end
|
|
1850
|
+
|
|
1851
|
+
# Issue #2174 — the subset of `signature_paths:` the user configured (every entry is also in
|
|
1852
|
+
# `@signature_paths`), and the features the project's source requires: read only by
|
|
1853
|
+
# {#ruby_library_reopen?}.
|
|
1854
|
+
def assign_reopen_inputs(project_signature_paths, required_features)
|
|
1855
|
+
@project_signature_paths = project_signature_paths.map { |p| Pathname(p) }.freeze
|
|
1856
|
+
@required_features = required_features.map { |feature| feature.to_s.dup.freeze }.freeze
|
|
1857
|
+
end
|
|
1858
|
+
private :assign_reopen_inputs
|
|
1859
|
+
private :ruby_library_roots
|
|
1860
|
+
|
|
1861
|
+
# The class / module names every one of whose declarations is the project's own
|
|
1862
|
+
# ({#project_authored_entry?}, which {#inline_only_entry?} asks behind its inline-only gate).
|
|
1863
|
+
def project_authored_classes
|
|
1864
|
+
@state[:project_authored_classes] ||= build_project_authored_classes
|
|
1865
|
+
end
|
|
1866
|
+
private :project_authored_classes
|
|
1867
|
+
|
|
1868
|
+
def build_project_authored_classes
|
|
1869
|
+
environment = env
|
|
1870
|
+
return Set.new.freeze if environment.nil?
|
|
1871
|
+
|
|
1872
|
+
# Only the user's configured `signature_paths:`: `@signature_paths` also holds plugin, gem-shipped,
|
|
1873
|
+
# rbs-collection and overlay directories, whose declarations are a library's own.
|
|
1874
|
+
project_files = self.class.project_sig_files(@project_signature_paths)
|
|
1875
|
+
virtual_names = @virtual_rbs.to_set(&:first)
|
|
1876
|
+
return Set.new.freeze if project_files.empty? && virtual_names.empty?
|
|
1877
|
+
|
|
1878
|
+
environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
|
|
1879
|
+
acc << rbs_name.to_s.delete_prefix("::") if project_authored_entry?(entry, virtual_names, project_files)
|
|
1880
|
+
end.freeze
|
|
1881
|
+
rescue ::RBS::BaseError
|
|
1882
|
+
Set.new.freeze
|
|
1883
|
+
end
|
|
1884
|
+
private :build_project_authored_classes
|
|
1885
|
+
|
|
1791
1886
|
# Issue #527 — the class / module names whose PRIMARY declaration lives in the `rbs` gem's own
|
|
1792
1887
|
# `core/` or `stdlib/` tree, top-level prefix stripped.
|
|
1793
1888
|
#
|
|
@@ -1808,9 +1903,23 @@ module Rigor
|
|
|
1808
1903
|
@state[:core_stdlib_classes] ||= build_core_stdlib_classes
|
|
1809
1904
|
end
|
|
1810
1905
|
|
|
1811
|
-
|
|
1906
|
+
# Issue #1154 — the subset of {#core_stdlib_classes} whose primary declaration is in `core/` alone: the classes
|
|
1907
|
+
# the interpreter implements, as opposed to a stdlib library written in Ruby (`Logger` rewrites `@level` in
|
|
1908
|
+
# its `initialize`). Same attribution, same cache behaviour.
|
|
1909
|
+
def core_class?(class_name)
|
|
1910
|
+
(@state[:core_classes] ||= build_core_stdlib_classes(CORE_ROOTS)).include?(class_name.to_s.delete_prefix("::"))
|
|
1911
|
+
end
|
|
1912
|
+
|
|
1913
|
+
CORE_ROOTS = begin
|
|
1914
|
+
[::RBS::EnvironmentLoader::DEFAULT_CORE_ROOT].compact.map { |root| "#{root}/".freeze }.freeze
|
|
1915
|
+
rescue StandardError
|
|
1916
|
+
[].freeze
|
|
1917
|
+
end
|
|
1918
|
+
private_constant :CORE_ROOTS
|
|
1919
|
+
|
|
1920
|
+
def build_core_stdlib_classes(roots = CORE_STDLIB_ROOTS)
|
|
1812
1921
|
environment = env
|
|
1813
|
-
return Set.new.freeze if environment.nil? ||
|
|
1922
|
+
return Set.new.freeze if environment.nil? || roots.empty?
|
|
1814
1923
|
|
|
1815
1924
|
names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
|
|
1816
1925
|
decl = self.class.primary_decl_for(entry)
|
|
@@ -1819,7 +1928,7 @@ module Rigor
|
|
|
1819
1928
|
path = self.class.declaration_buffer_name(decl)
|
|
1820
1929
|
next if path.nil?
|
|
1821
1930
|
|
|
1822
|
-
acc << rbs_name.to_s.delete_prefix("::") if
|
|
1931
|
+
acc << rbs_name.to_s.delete_prefix("::") if roots.any? { |root| path.start_with?(root) }
|
|
1823
1932
|
end
|
|
1824
1933
|
names.freeze
|
|
1825
1934
|
rescue ::RBS::BaseError
|