rigortype 0.3.5 → 0.3.7
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 +5 -5
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -10,6 +10,8 @@ require_relative "../scope"
|
|
|
10
10
|
require_relative "../source/node_locator"
|
|
11
11
|
require_relative "../inference/fallback_tracer"
|
|
12
12
|
require_relative "../inference/scope_indexer"
|
|
13
|
+
require_relative "../inference/precision_scanner"
|
|
14
|
+
require_relative "../source/node_children"
|
|
13
15
|
require_relative "type_of_renderer"
|
|
14
16
|
require_relative "command"
|
|
15
17
|
require_relative "options"
|
|
@@ -26,9 +28,23 @@ module Rigor
|
|
|
26
28
|
# type-of UX (extra flags, watch mode, streaming output) without bloating the CLI shell. Output formatting is
|
|
27
29
|
# delegated to {TypeOfRenderer}.
|
|
28
30
|
class TypeOfCommand < Command
|
|
29
|
-
USAGE = "Usage: rigor type-of [options] FILE:LINE:COL"
|
|
31
|
+
USAGE = "Usage: rigor type-of [options] FILE:LINE[:COL] [FILE:LINE[:COL] ...]"
|
|
30
32
|
|
|
31
|
-
Result = Data.define(:file, :line, :column, :node, :type, :tracer)
|
|
33
|
+
Result = Data.define(:file, :line, :column, :node, :type, :tracer, :enumeration) do
|
|
34
|
+
def initialize(enumeration: nil, **rest)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
LineEnumeration = Data.define(:total, :shown)
|
|
40
|
+
|
|
41
|
+
# One requested position. A nil `column` means "the first 40 expressions starting on this line" — the form that
|
|
42
|
+
# exists because hand-computing a column is the single most error-prone part of using this command.
|
|
43
|
+
Target = Data.define(:file, :line, :column)
|
|
44
|
+
|
|
45
|
+
# Cap on the expressions one bare `FILE:LINE` prints, so a long line cannot bury the answer.
|
|
46
|
+
LINE_ENUMERATION_CAP = 40
|
|
47
|
+
private_constant :LINE_ENUMERATION_CAP
|
|
32
48
|
|
|
33
49
|
# @return [Integer] CLI exit status.
|
|
34
50
|
def run
|
|
@@ -36,10 +52,10 @@ module Rigor
|
|
|
36
52
|
buffer = Options.resolve_buffer_binding(options, err: @err)
|
|
37
53
|
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
return CLI::EXIT_USAGE if
|
|
55
|
+
targets = parse_position_arguments(@argv)
|
|
56
|
+
return CLI::EXIT_USAGE if targets.nil?
|
|
41
57
|
|
|
42
|
-
execute(
|
|
58
|
+
execute(targets: targets, options: options, buffer: buffer)
|
|
43
59
|
end
|
|
44
60
|
|
|
45
61
|
private
|
|
@@ -59,38 +75,123 @@ module Rigor
|
|
|
59
75
|
options
|
|
60
76
|
end
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
# One process, N positions. Every invocation used to rebuild the plugin-aware environment and reparse the
|
|
79
|
+
# file for a single answer, so investigating a chain of expressions cost one process each — the dominant
|
|
80
|
+
# cost of using this command in anger. The environment is now built once for the whole request and each
|
|
81
|
+
# file is parsed and scope-indexed once, however many positions land in it.
|
|
82
|
+
def execute(targets:, options:, buffer: nil)
|
|
83
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
84
|
+
environment = project_environment(targets.map(&:file).uniq, configuration)
|
|
85
|
+
base_scope = Scope.empty(environment: environment)
|
|
86
|
+
|
|
87
|
+
results_by_target = Array.new(targets.length)
|
|
88
|
+
targets.each_with_index.group_by { |target, _index| target.file }.each do |file, indexed_targets|
|
|
89
|
+
file_results = resolve_file(file, indexed_targets, configuration, base_scope, options, buffer)
|
|
90
|
+
return file_results if file_results.is_a?(Integer)
|
|
91
|
+
|
|
92
|
+
file_results.each { |index, resolved| results_by_target[index] = resolved }
|
|
93
|
+
end
|
|
94
|
+
results = results_by_target.compact.flatten(1)
|
|
95
|
+
return 1 if results.empty?
|
|
96
|
+
|
|
97
|
+
TypeOfRenderer.new(out: @out).render(results, format: options.fetch(:format))
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Every result for one file: parsed and indexed once, then each requested position resolved against that
|
|
102
|
+
# index. Returns an Integer exit status instead when the file itself cannot be probed.
|
|
103
|
+
def resolve_file(file, indexed_targets, configuration, base_scope, options, buffer)
|
|
104
|
+
# Under editor mode the logical `file` may not exist on disk (user editing a new file); the runtime check
|
|
105
|
+
# is only that the BUFFER is readable, which `resolve_buffer_binding` has already enforced.
|
|
67
106
|
physical = buffer ? buffer.resolve(file) : file
|
|
68
107
|
return 1 unless file_exists?(buffer ? physical : file)
|
|
69
108
|
|
|
70
|
-
configuration = Configuration.load(options.fetch(:config))
|
|
71
109
|
source = File.read(physical)
|
|
72
110
|
parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
|
|
73
111
|
return 1 if parse_errors?(parse_result, file)
|
|
74
112
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
# Built with no tracer attached — it would otherwise double-record fallback events with the per-node
|
|
114
|
+
# `type_of` calls below.
|
|
115
|
+
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
116
|
+
locator = Source::NodeLocator.new(source: source, root: parse_result.value)
|
|
117
|
+
lines = indexed_targets.filter_map { |target, _index| target.line if target.column.nil? }
|
|
118
|
+
line_nodes, line_totals = collect_line_nodes(parse_result.value, lines)
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
resolve_targets(file: file, indexed_targets: indexed_targets, locator: locator, line_nodes: line_nodes,
|
|
121
|
+
line_totals: line_totals, scope_index: scope_index, options: options)
|
|
122
|
+
end
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
def resolve_targets(file:, indexed_targets:, locator:, line_nodes:, line_totals:, scope_index:, options:)
|
|
125
|
+
indexed_targets.map do |target, index|
|
|
126
|
+
if target.column.nil?
|
|
127
|
+
resolved = enumerate_line(file, target.line, line_nodes.fetch(target.line),
|
|
128
|
+
line_totals.fetch(target.line), scope_index, options)
|
|
129
|
+
next [index, resolved]
|
|
130
|
+
end
|
|
88
131
|
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
node = locate_node(locator: locator, file: file, line: target.line, column: target.column)
|
|
133
|
+
return CLI::EXIT_USAGE if node == :out_of_range
|
|
134
|
+
next [index, []] if node.nil?
|
|
91
135
|
|
|
92
|
-
|
|
93
|
-
|
|
136
|
+
[index, [type_result(file, target.line, target.column, node, scope_index, options)]]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Collect every requested line in one full syntax walk. Unlike `Source::NodeWalker`, this deliberately
|
|
141
|
+
# descends into `defined?`: the probe inventories source expressions, including operands Ruby inspects but
|
|
142
|
+
# does not evaluate. Candidate storage stays bounded while retaining the first 40 nodes in display order.
|
|
143
|
+
def collect_line_nodes(root, lines)
|
|
144
|
+
return [{}, {}] if lines.empty?
|
|
145
|
+
|
|
146
|
+
candidates = lines.uniq.to_h { |line| [line, []] }
|
|
147
|
+
totals = candidates.to_h { |line, _nodes| [line, 0] }
|
|
148
|
+
walk_syntax(root) do |node|
|
|
149
|
+
line = node.location.start_line
|
|
150
|
+
nodes = candidates[line]
|
|
151
|
+
next if nodes.nil?
|
|
152
|
+
next if Inference::PrecisionScanner::NON_EXPRESSION_NODE_TYPES.include?(node.class.name)
|
|
153
|
+
|
|
154
|
+
totals[line] += 1
|
|
155
|
+
nodes << node
|
|
156
|
+
trim_line_nodes(nodes) if nodes.length > LINE_ENUMERATION_CAP * 2
|
|
157
|
+
end
|
|
158
|
+
candidates.each_value { |nodes| trim_line_nodes(nodes) }
|
|
159
|
+
[candidates, totals]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def walk_syntax(node, &block)
|
|
163
|
+
return unless node.is_a?(Prism::Node)
|
|
164
|
+
|
|
165
|
+
block.call(node)
|
|
166
|
+
node.rigor_each_child { |child| walk_syntax(child, &block) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def trim_line_nodes(nodes)
|
|
170
|
+
nodes.sort_by! { |node| [node.location.start_column, -node.location.length] }
|
|
171
|
+
nodes.slice!(LINE_ENUMERATION_CAP, nodes.length)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Up to 40 expressions STARTING on `line`, outermost first at each 1-based column. This form removes
|
|
175
|
+
# the column arithmetic: `rigor type-of file.rb:42` answers "what are the types on line 42" without the user
|
|
176
|
+
# having to guess which byte the receiver starts at.
|
|
177
|
+
def enumerate_line(file, line, nodes, total, scope_index, options)
|
|
178
|
+
if nodes.empty?
|
|
179
|
+
@err.puts("type-of: no expression found on #{file}:#{line}")
|
|
180
|
+
return []
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
enumeration = LineEnumeration.new(total: total, shown: nodes.length)
|
|
184
|
+
nodes.map do |node|
|
|
185
|
+
type_result(file, line, node.location.start_column + 1, node, scope_index, options,
|
|
186
|
+
enumeration: enumeration)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def type_result(file, line, column, node, scope_index, options, enumeration: nil)
|
|
191
|
+
tracer = options[:trace] ? Inference::FallbackTracer.new : nil
|
|
192
|
+
type = scope_index[node].type_of(node, tracer: tracer)
|
|
193
|
+
Result.new(file: file, line: line, column: column, node: node, type: type, tracer: tracer,
|
|
194
|
+
enumeration: enumeration)
|
|
94
195
|
end
|
|
95
196
|
|
|
96
197
|
# Builds the plugin-aware environment relative to the probed file, so the reported type matches what `rigor
|
|
@@ -99,8 +200,8 @@ module Rigor
|
|
|
99
200
|
# closes). The probed file is threaded as the synthesizer's `source_files:`. Project-RBS auto-detection
|
|
100
201
|
# roots at CWD today; future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec`
|
|
101
202
|
# so probes against files outside the current process's CWD still see the right `sig/` tree.
|
|
102
|
-
def project_environment(
|
|
103
|
-
ProbeEnvironment.build(configuration: configuration, source_files:
|
|
203
|
+
def project_environment(files, configuration)
|
|
204
|
+
ProbeEnvironment.build(configuration: configuration, source_files: files)
|
|
104
205
|
end
|
|
105
206
|
|
|
106
207
|
def file_exists?(file)
|
|
@@ -117,8 +218,8 @@ module Rigor
|
|
|
117
218
|
true
|
|
118
219
|
end
|
|
119
220
|
|
|
120
|
-
def locate_node(
|
|
121
|
-
node =
|
|
221
|
+
def locate_node(locator:, file:, line:, column:)
|
|
222
|
+
node = locator.at_position(line: line, column: column)
|
|
122
223
|
@err.puts("type-of: no expression found at #{file}:#{line}:#{column}") if node.nil?
|
|
123
224
|
node
|
|
124
225
|
rescue Source::NodeLocator::OutOfRangeError => e
|
|
@@ -126,19 +227,66 @@ module Rigor
|
|
|
126
227
|
:out_of_range
|
|
127
228
|
end
|
|
128
229
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
parse_colon_form(argv[0])
|
|
133
|
-
when 3
|
|
134
|
-
decode_position(*argv)
|
|
135
|
-
else
|
|
136
|
-
@err.puts("type-of: expected FILE:LINE:COL or FILE LINE COL")
|
|
230
|
+
def parse_position_arguments(argv)
|
|
231
|
+
if argv.empty?
|
|
232
|
+
@err.puts("type-of: expected FILE:LINE[:COL] or FILE LINE COL")
|
|
137
233
|
@err.puts(USAGE)
|
|
138
|
-
nil
|
|
234
|
+
return nil
|
|
139
235
|
end
|
|
236
|
+
|
|
237
|
+
colon = argv.map { |arg| try_colon_form(arg) }
|
|
238
|
+
return colon if colon.all?
|
|
239
|
+
return legacy_triple(argv) if argv.size == 3 && colon.none?
|
|
240
|
+
# A single unparseable argument keeps its old, specific diagnosis ("must be integers", "expected
|
|
241
|
+
# FILE:LINE:COL") rather than the generic multi-position complaint below.
|
|
242
|
+
return single_colon_form(argv[0]) if argv.size == 1
|
|
243
|
+
|
|
244
|
+
@err.puts("type-of: expected FILE:LINE[:COL] (repeatable) or FILE LINE COL, got #{argv.inspect}")
|
|
245
|
+
@err.puts(USAGE)
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# `FILE:LINE:COL` or `FILE:LINE`, or nil when the argument is not in either form. Silent: the caller
|
|
250
|
+
# decides whether a nil means "try the three-argument form" or "report a usage error", and a probe should
|
|
251
|
+
# not print a complaint about a shape it is about to accept under a different reading.
|
|
252
|
+
def try_colon_form(arg)
|
|
253
|
+
parts = arg.split(":")
|
|
254
|
+
return nil if parts.size < 2 || !integer_literal?(parts[-1])
|
|
255
|
+
|
|
256
|
+
# Three or more segments is the `FILE:LINE:COL` reading; if its LINE is not a number the argument is a
|
|
257
|
+
# typo, not a column-less position, and must reach the specific "must be integers" diagnosis.
|
|
258
|
+
if parts.size >= 3
|
|
259
|
+
return nil unless integer_literal?(parts[-2])
|
|
260
|
+
|
|
261
|
+
Target.new(file: parts[0..-3].join(":"), line: Integer(parts[-2], 10), column: Integer(parts[-1], 10))
|
|
262
|
+
else
|
|
263
|
+
Target.new(file: parts[0], line: Integer(parts[-1], 10), column: nil)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def integer_literal?(value)
|
|
268
|
+
value.match?(/\A\d+\z/)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# The pre-batch single-argument path, kept for its error messages. Returns nil after printing one.
|
|
272
|
+
def single_colon_form(arg)
|
|
273
|
+
position = parse_colon_form(arg)
|
|
274
|
+
return nil if position.nil?
|
|
275
|
+
|
|
276
|
+
file, line, column = position
|
|
277
|
+
[Target.new(file: file, line: line, column: column)]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def legacy_triple(argv)
|
|
281
|
+
position = decode_position(*argv)
|
|
282
|
+
return nil if position.nil?
|
|
283
|
+
|
|
284
|
+
file, line, column = position
|
|
285
|
+
[Target.new(file: file, line: line, column: column)]
|
|
140
286
|
end
|
|
141
287
|
|
|
288
|
+
# The strict `FILE:LINE:COL` reading, used only to produce the specific error for a single unparseable
|
|
289
|
+
# argument. The permissive reading (which also accepts `FILE:LINE`) is {try_colon_form}.
|
|
142
290
|
def parse_colon_form(arg)
|
|
143
291
|
parts = arg.split(":")
|
|
144
292
|
if parts.size < 3
|
|
@@ -149,8 +297,7 @@ module Rigor
|
|
|
149
297
|
|
|
150
298
|
column = parts.pop
|
|
151
299
|
line = parts.pop
|
|
152
|
-
|
|
153
|
-
decode_position(file, line, column)
|
|
300
|
+
decode_position(parts.join(":"), line, column)
|
|
154
301
|
end
|
|
155
302
|
|
|
156
303
|
def decode_position(file, line, column)
|
|
@@ -20,7 +20,55 @@ module Rigor
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Takes the whole result list: one invocation can now answer several positions, and a bare `FILE:LINE`
|
|
24
|
+
# answers up to 40 expressions on that line. A single exact result renders byte-identically to before.
|
|
25
|
+
def render_text(results)
|
|
26
|
+
list = Array(results)
|
|
27
|
+
index = 0
|
|
28
|
+
first = true
|
|
29
|
+
while index < list.length
|
|
30
|
+
@out.puts("") unless first
|
|
31
|
+
result = list[index]
|
|
32
|
+
enumeration = result.enumeration
|
|
33
|
+
if enumeration
|
|
34
|
+
finish = index + 1
|
|
35
|
+
finish += 1 while finish < list.length && list[finish].enumeration.equal?(enumeration)
|
|
36
|
+
render_enumeration(list[index...finish], enumeration)
|
|
37
|
+
index = finish
|
|
38
|
+
else
|
|
39
|
+
render_one_text(result)
|
|
40
|
+
index += 1
|
|
41
|
+
end
|
|
42
|
+
first = false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A bare `FILE:LINE` asks "what are the types on this line", and the answer is a table: one row per
|
|
47
|
+
# expression, outermost first at each column, so a method chain reads top to bottom.
|
|
48
|
+
def render_enumeration(results, enumeration)
|
|
49
|
+
@out.puts("#{results.first.file}:#{results.first.line}")
|
|
50
|
+
render_enumeration_rows(results)
|
|
51
|
+
if enumeration.total > results.length
|
|
52
|
+
omitted = enumeration.total - results.length
|
|
53
|
+
@out.puts(" ... #{omitted} additional expressions omitted (limit #{results.length})")
|
|
54
|
+
end
|
|
55
|
+
# After the table, never interleaved with it: a `--trace` fallback list between two rows would break the
|
|
56
|
+
# column alignment the whole form exists for.
|
|
57
|
+
results.each { |result| render_text_fallbacks(result) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_enumeration_rows(results)
|
|
61
|
+
width = results.map { |result| node_label(result).length }.max
|
|
62
|
+
results.each do |result|
|
|
63
|
+
@out.puts(format(" %4d %-#{width}s %s", result.column, node_label(result), result.type.describe))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def node_label(result)
|
|
68
|
+
result.node.class.name.to_s.delete_prefix("Prism::")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_one_text(result)
|
|
24
72
|
@out.puts("#{result.file}:#{result.line}:#{result.column}")
|
|
25
73
|
@out.puts("node: #{result.node.class}")
|
|
26
74
|
@out.puts("type: #{result.type.describe}")
|
|
@@ -40,7 +88,28 @@ module Rigor
|
|
|
40
88
|
end
|
|
41
89
|
end
|
|
42
90
|
|
|
43
|
-
|
|
91
|
+
# A single result keeps the flat object it has always emitted, so existing consumers are untouched; a
|
|
92
|
+
# multi-position request wraps them in `results` rather than printing several documents to one stream.
|
|
93
|
+
def render_json(results)
|
|
94
|
+
list = Array(results)
|
|
95
|
+
payload = list.size == 1 ? result_to_h(list.first) : { results: list.map { |r| result_to_h(r) } }
|
|
96
|
+
enumerations = line_enumerations(list)
|
|
97
|
+
payload[:line_enumerations] = enumerations unless enumerations.empty?
|
|
98
|
+
@out.puts(JSON.pretty_generate(payload))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def line_enumerations(results)
|
|
102
|
+
seen = {}.compare_by_identity
|
|
103
|
+
results.filter_map do |result|
|
|
104
|
+
enumeration = result.enumeration
|
|
105
|
+
next if enumeration.nil? || seen.key?(enumeration)
|
|
106
|
+
|
|
107
|
+
seen[enumeration] = true
|
|
108
|
+
{ file: result.file, line: result.line, shown: enumeration.shown, total: enumeration.total }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def result_to_h(result)
|
|
44
113
|
payload = {
|
|
45
114
|
file: result.file,
|
|
46
115
|
line: result.line,
|
|
@@ -50,7 +119,7 @@ module Rigor
|
|
|
50
119
|
erased: result.type.erase_to_rbs
|
|
51
120
|
}
|
|
52
121
|
payload[:fallbacks] = result.tracer.map { |event| fallback_to_h(event) } if result.tracer
|
|
53
|
-
|
|
122
|
+
payload
|
|
54
123
|
end
|
|
55
124
|
|
|
56
125
|
def format_fallback_text(event, file)
|
|
@@ -12,6 +12,7 @@ require_relative "type_scan_renderer"
|
|
|
12
12
|
require_relative "type_scan_report"
|
|
13
13
|
require_relative "command"
|
|
14
14
|
require_relative "probe_environment"
|
|
15
|
+
require_relative "coverage_scan"
|
|
15
16
|
|
|
16
17
|
module Rigor
|
|
17
18
|
class CLI
|
|
@@ -70,7 +71,15 @@ module Rigor
|
|
|
70
71
|
|
|
71
72
|
def scan_paths(paths, options)
|
|
72
73
|
configuration = Configuration.load(options.fetch(:config))
|
|
73
|
-
|
|
74
|
+
# Same seed set as `rigor coverage` and `coverage --protection` (#502). Without it a cross-file class
|
|
75
|
+
# constant reads `Dynamic` and is counted as an unrecognised node, so the scan reports the engine as
|
|
76
|
+
# blinder than it is — on Mastodon that alone was most of a 14.7% unrecognised rate.
|
|
77
|
+
scope = CoverageScan.discovery_seeded_scope(
|
|
78
|
+
files: paths,
|
|
79
|
+
configuration: configuration,
|
|
80
|
+
environment: project_environment(configuration, paths),
|
|
81
|
+
parameter_inference: configuration.parameter_inference
|
|
82
|
+
)
|
|
74
83
|
scanner = Inference::CoverageScanner.new(scope: scope)
|
|
75
84
|
accumulator = ScanAccumulator.new
|
|
76
85
|
paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
|
|
@@ -6,10 +6,12 @@ require "json"
|
|
|
6
6
|
require_relative "../configuration"
|
|
7
7
|
require_relative "../analysis/path_expansion"
|
|
8
8
|
require_relative "../analysis/reachability/scan"
|
|
9
|
+
require_relative "../analysis/reachability/scan_cache"
|
|
9
10
|
require_relative "../analysis/reachability/graph"
|
|
10
11
|
require_relative "../analysis/reachability/plugin_roots"
|
|
11
12
|
require_relative "../analysis/reachability/signature_scan"
|
|
12
13
|
require_relative "../analysis/reachability/project_files"
|
|
14
|
+
require_relative "../cache/store"
|
|
13
15
|
require_relative "options"
|
|
14
16
|
require_relative "command"
|
|
15
17
|
require_relative "probe_environment"
|
|
@@ -22,7 +24,11 @@ module Rigor
|
|
|
22
24
|
# measured precision of this signal is 7.0% on an adjudicated corpus target
|
|
23
25
|
# (`docs/notes/20260813-unused-constant-fp-baseline.md`), so its output is a review queue, not a defect
|
|
24
26
|
# list, and it never enters `rigor check`'s stream at any severity (WD1). Exits 0 whatever it finds.
|
|
25
|
-
|
|
27
|
+
#
|
|
28
|
+
# Like {CheckCommand}, it aggregates one command's concerns — the two scans, the graph, and both
|
|
29
|
+
# render formats — that read clearer together than split across micro-classes, so it carries the
|
|
30
|
+
# same ClassLength exemption.
|
|
31
|
+
class UnusedCommand < Command # rubocop:disable Metrics/ClassLength
|
|
26
32
|
USAGE = "Usage: rigor unused [options] [paths]"
|
|
27
33
|
|
|
28
34
|
# WD7 — the REFERENCE corpus is wider than the ANALYSIS corpus. `.rake` files sit inside `paths:` and
|
|
@@ -43,23 +49,32 @@ module Rigor
|
|
|
43
49
|
|
|
44
50
|
configuration = Configuration.load(options.fetch(:config))
|
|
45
51
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
46
|
-
|
|
52
|
+
scan_cache = Analysis::Reachability::ScanCache.open(configuration.cache_path,
|
|
53
|
+
target_ruby: configuration.target_ruby)
|
|
54
|
+
declarations, references, dynamic_uses = scan(paths, configuration, scan_cache)
|
|
47
55
|
references.concat(signature_references(configuration))
|
|
48
|
-
dynamic_uses.concat(template_mentions(declarations))
|
|
56
|
+
dynamic_uses.concat(template_mentions(declarations, scan_cache))
|
|
49
57
|
|
|
50
|
-
contribution = Analysis::Reachability::PluginRoots.collect(configuration: configuration
|
|
58
|
+
contribution = Analysis::Reachability::PluginRoots.collect(configuration: configuration,
|
|
59
|
+
cache_store: cache_store(configuration))
|
|
51
60
|
references.concat(plugin_references(contribution.references))
|
|
52
|
-
graph =
|
|
53
|
-
|
|
54
|
-
root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
|
|
55
|
-
foreign: foreign_predicate(configuration)
|
|
56
|
-
)
|
|
61
|
+
graph = build_graph(configuration, options, contribution,
|
|
62
|
+
declarations: declarations, references: references, dynamic_uses: dynamic_uses)
|
|
57
63
|
emit(graph.report, options, supply: root_supply(contribution.roots, declarations))
|
|
64
|
+
scan_cache.save
|
|
58
65
|
0
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
private
|
|
62
69
|
|
|
70
|
+
def build_graph(configuration, options, contribution, declarations:, references:, dynamic_uses:)
|
|
71
|
+
Analysis::Reachability::Graph.new(
|
|
72
|
+
declarations: declarations, references: references, dynamic_uses: dynamic_uses,
|
|
73
|
+
root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
|
|
74
|
+
foreign: foreign_predicate(configuration)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
63
78
|
def parse_options
|
|
64
79
|
options = { config: nil, format: "text", entry_points: [], limit: nil }
|
|
65
80
|
parser = OptionParser.new do |opts|
|
|
@@ -91,13 +106,15 @@ module Rigor
|
|
|
91
106
|
end
|
|
92
107
|
|
|
93
108
|
# Declarations come from the analysed paths; references additionally from the wider corpus (WD7).
|
|
94
|
-
|
|
109
|
+
# Every file is consulted every run — an unchanged one contributes its cached scan, so the
|
|
110
|
+
# whole-project completeness the `--incremental` refusal protects is unaffected.
|
|
111
|
+
def scan(paths, configuration, cache)
|
|
95
112
|
declaration_files = Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns).to_set
|
|
96
113
|
declarations = []
|
|
97
114
|
references = []
|
|
98
115
|
dynamic_uses = []
|
|
99
116
|
(declaration_files + reference_files(paths, configuration)).sort.each do |file|
|
|
100
|
-
result = read_and_scan(file, configuration)
|
|
117
|
+
result = cache.serve(:scan, file) { read_and_scan(file, configuration) }
|
|
101
118
|
next if result.nil?
|
|
102
119
|
|
|
103
120
|
declarations.concat(result.declarations) if declaration_files.include?(file)
|
|
@@ -155,24 +172,38 @@ module Rigor
|
|
|
155
172
|
end
|
|
156
173
|
end
|
|
157
174
|
|
|
158
|
-
def template_mentions(declarations)
|
|
175
|
+
def template_mentions(declarations, cache)
|
|
159
176
|
names = declarations.map(&:fqn)
|
|
160
177
|
return [] if names.empty?
|
|
161
178
|
|
|
162
179
|
Analysis::Reachability::ProjectFiles.own(Dir.glob(TEMPLATE_GLOB, base: Dir.pwd), Dir.pwd).flat_map do |rel|
|
|
163
|
-
|
|
180
|
+
absolute = File.expand_path(rel)
|
|
181
|
+
haystack = cache.serve(:runs, absolute) { constant_bearing_text(File.read(absolute).scrub) }
|
|
164
182
|
names.filter_map do |fqn|
|
|
165
|
-
next unless
|
|
183
|
+
next unless haystack.include?(fqn)
|
|
166
184
|
|
|
167
185
|
Analysis::Reachability::Scan::DynamicUse.new(name: nil, prefix: fqn, site: nil,
|
|
168
186
|
reason: "named as a string in #{rel}",
|
|
169
|
-
path: rel, line: 1)
|
|
187
|
+
path: rel, line: 1, scope: :exact)
|
|
170
188
|
end
|
|
171
189
|
rescue SystemCallError, ArgumentError
|
|
172
190
|
[]
|
|
173
191
|
end
|
|
174
192
|
end
|
|
175
193
|
|
|
194
|
+
# A maximal run of constant-path characters carrying at least one capital — the only substrings a
|
|
195
|
+
# declaration FQN can occur inside, since an FQN starts with a capital and uses this charset alone.
|
|
196
|
+
# An occurrence cannot cross a non-charset character, so `include?` over the de-duplicated runs
|
|
197
|
+
# (joined by a character outside the charset) answers exactly what `include?` over the whole text
|
|
198
|
+
# did, on a fraction of the bytes: a locale YAML or fixture JSON is almost entirely lowercase prose,
|
|
199
|
+
# and scanning 21 MB of it per declaration name was 81% of this command's wall time on mastodon.
|
|
200
|
+
CONSTANT_BEARING_RUN = /[A-Za-z0-9_:]*[A-Z][A-Za-z0-9_:]*/
|
|
201
|
+
private_constant :CONSTANT_BEARING_RUN
|
|
202
|
+
|
|
203
|
+
def constant_bearing_text(text)
|
|
204
|
+
text.scan(CONSTANT_BEARING_RUN).uniq.join("\n")
|
|
205
|
+
end
|
|
206
|
+
|
|
176
207
|
def root_fqns(declarations, globs)
|
|
177
208
|
return [] if globs.empty?
|
|
178
209
|
|
|
@@ -188,13 +219,31 @@ module Rigor
|
|
|
188
219
|
# declaration, which produced three of redmine's artifacts from a single initializer. A name the bundled
|
|
189
220
|
# (non-project) environment already knows is not ours to call unused. The project's own `sig/` is
|
|
190
221
|
# deliberately excluded from this environment, so a project class that ships a signature stays owned.
|
|
222
|
+
#
|
|
223
|
+
# The cache store makes the environment a Marshal restore instead of a cold RBS build on every
|
|
224
|
+
# invocation — the same ADR-54 slot `rigor check` reads, keyed apart by this environment's own
|
|
225
|
+
# (sig-less) descriptor. The report only ever asks "is this name known", which the cached
|
|
226
|
+
# environment answers identically: the one thing the cache degrades is `RBS::Location`, and no
|
|
227
|
+
# location is read here.
|
|
191
228
|
def foreign_predicate(configuration)
|
|
192
|
-
env = Environment.for_project(libraries: configuration.libraries, signature_paths: []
|
|
229
|
+
env = Environment.for_project(libraries: configuration.libraries, signature_paths: [],
|
|
230
|
+
cache_store: cache_store(configuration))
|
|
193
231
|
->(fqn) { !env.singleton_for_name(fqn).nil? }
|
|
194
232
|
rescue StandardError
|
|
195
233
|
->(_fqn) { false }
|
|
196
234
|
end
|
|
197
235
|
|
|
236
|
+
# One store per run, shared by the environment build and the plugin-roots collection. Nil when the
|
|
237
|
+
# store cannot be opened — both consumers already treat a nil store as "recompute", which was this
|
|
238
|
+
# command's only mode before it had a cache at all.
|
|
239
|
+
def cache_store(configuration)
|
|
240
|
+
return @cache_store if defined?(@cache_store)
|
|
241
|
+
|
|
242
|
+
@cache_store = Cache::Store.new(root: configuration.cache_path)
|
|
243
|
+
rescue StandardError
|
|
244
|
+
@cache_store = nil
|
|
245
|
+
end
|
|
246
|
+
|
|
198
247
|
# ADR-102 § Consequences — "a root source that OVER-supplies silently hides real dead code, which is
|
|
199
248
|
# worse than one that under-supplies, so each plugin's contribution needs its own corpus check". A
|
|
200
249
|
# supplied root naming a constant the project does not declare is inert in the graph, but it is the
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -101,13 +101,26 @@ module Rigor
|
|
|
101
101
|
|
|
102
102
|
def dispatch(command)
|
|
103
103
|
handler = HANDLERS[command]
|
|
104
|
-
return send(handler) if handler
|
|
104
|
+
return arm_jit_deadline { send(handler) } if handler
|
|
105
105
|
|
|
106
106
|
@err.puts("Unknown command: #{command}")
|
|
107
107
|
@err.puts(help)
|
|
108
108
|
EXIT_USAGE
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Deferred YJIT for EVERY dispatched command, not just `check` / `coverage` where PR #75 first
|
|
112
|
+
# calibrated it. The deadline makes the decision command-independent: a run that finishes inside
|
|
113
|
+
# the window never pays JIT compile, and a run that outlasts it JITs its dominant tail — measured
|
|
114
|
+
# on `rigor effects` cold over Mastodon, which ran its whole 21 s interpreted while the same
|
|
115
|
+
# analysis under `check` took 14.8 s (forcing YJIT on the effects run: 15.9 s; disabling it on
|
|
116
|
+
# check: 21.1 s). `lsp` / `mcp` still call `Runtime::Jit.enable_now` at boot, which makes the
|
|
117
|
+
# deadline thread armed here a no-op when it later fires.
|
|
118
|
+
def arm_jit_deadline
|
|
119
|
+
require_relative "runtime/jit"
|
|
120
|
+
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
121
|
+
yield
|
|
122
|
+
end
|
|
123
|
+
|
|
111
124
|
def run_check
|
|
112
125
|
require_relative "cli/check_command"
|
|
113
126
|
|
|
@@ -363,17 +376,17 @@ module Rigor
|
|
|
363
376
|
check Analyze Ruby source files
|
|
364
377
|
init Create a starter .rigor.yml
|
|
365
378
|
annotate Print FILE with each line's last-expression type
|
|
366
|
-
type-of Print
|
|
379
|
+
type-of Print inferred types at FILE:LINE[:COL] positions
|
|
367
380
|
trace Replay how the engine typed FILE as a terminal animation
|
|
368
381
|
type-scan Report Scope#type_of coverage across PATHs
|
|
369
382
|
effects Report each method's effect labels, and the committed effect snapshot
|
|
370
|
-
(
|
|
383
|
+
(opt-in; effects update/check/diff/explain)
|
|
371
384
|
explain Print the description of one or all CheckRules
|
|
372
385
|
diff Compare current diagnostics to a saved baseline JSON
|
|
373
|
-
sig-gen Emit RBS skeletons inferred from .rb sources
|
|
386
|
+
sig-gen Emit RBS skeletons inferred from .rb sources
|
|
374
387
|
lsp Run the Rigor Language Server (LSP) over stdio
|
|
375
|
-
mcp Run the Rigor MCP server over stdio
|
|
376
|
-
triage Summarise diagnostics: distribution, hotspots, hints
|
|
388
|
+
mcp Run the Rigor MCP server over stdio
|
|
389
|
+
triage Summarise diagnostics: distribution, hotspots, hints
|
|
377
390
|
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
378
391
|
plugins Report activation status of every configured plugin
|
|
379
392
|
plugin Browse bundled plugin source as worked examples (list/path/print/root)
|
|
@@ -381,9 +394,9 @@ module Rigor
|
|
|
381
394
|
describe Recommend the next skill for this project (alias for `skill describe`)
|
|
382
395
|
skill Recommend the next skill + list/print bundled Agent Skills (skill describe, skill <name>)
|
|
383
396
|
docs Print the bundled docs offline (docs <name>, docs --list)
|
|
384
|
-
show-bleedingedge Show the bleeding-edge overlay + what your config adopts
|
|
385
|
-
doctor Classify setup problems vs clean run with routed next actions
|
|
386
|
-
upgrade Migration command skeleton (
|
|
397
|
+
show-bleedingedge Show the bleeding-edge overlay + what your config adopts
|
|
398
|
+
doctor Classify setup problems vs clean run with routed next actions
|
|
399
|
+
upgrade Migration command skeleton (queued)
|
|
387
400
|
version Print the Rigor version
|
|
388
401
|
help Print this help
|
|
389
402
|
HELP
|