rigortype 0.3.6 → 0.3.8
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/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 +409 -11
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +23 -7
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- 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 +188 -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/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +63 -16
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
- data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
- data/lib/rigor/analysis/runner.rb +212 -16
- data/lib/rigor/analysis/worker_session.rb +37 -3
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
- data/lib/rigor/cache/store.rb +17 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- 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 +47 -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.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/failure_slot.rb +28 -0
- 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 +743 -93
- data/lib/rigor/environment.rb +125 -25
- 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 +61 -3
- data/lib/rigor/inference/hkt_sugar_translator.rb +87 -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/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/plugin/registry.rb +25 -2
- 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 +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- 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 +320 -85
- data/lib/rigor/sig_gen/renderer.rb +14 -12
- 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 +9 -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
- metadata +71 -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) }
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -376,7 +376,7 @@ module Rigor
|
|
|
376
376
|
check Analyze Ruby source files
|
|
377
377
|
init Create a starter .rigor.yml
|
|
378
378
|
annotate Print FILE with each line's last-expression type
|
|
379
|
-
type-of Print
|
|
379
|
+
type-of Print inferred types at FILE:LINE[:COL] positions
|
|
380
380
|
trace Replay how the engine typed FILE as a terminal animation
|
|
381
381
|
type-scan Report Scope#type_of coverage across PATHs
|
|
382
382
|
effects Report each method's effect labels, and the committed effect snapshot
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -144,10 +144,13 @@ module Rigor
|
|
|
144
144
|
},
|
|
145
145
|
"bundler" => {
|
|
146
146
|
# Open item O4 — target-project Bundler awareness. When `bundle_path:` is set (or auto-detected),
|
|
147
|
-
# Rigor walks `<bundle_path>/ruby/*/gems/*/sig/`
|
|
148
|
-
# `
|
|
149
|
-
#
|
|
150
|
-
#
|
|
147
|
+
# Rigor walks `<bundle_path>/ruby/*/gems/*/sig/` (RubyGems-sourced gems) and
|
|
148
|
+
# `<bundle_path>/ruby/*/bundler/gems/*/sig/` (`git:`-sourced gems) and adds each gem-shipped sig
|
|
149
|
+
# directory to `signature_paths:`. A `path:`-sourced gem is not under either — Bundler uses the
|
|
150
|
+
# `path:` directory in place rather than copying it into the bundle root — so add its `sig/` directly
|
|
151
|
+
# via `signature_paths:` instead. With O7's failure-memo in place, conflicts (a vendored sig already
|
|
152
|
+
# declares the same constant) degrade gracefully to "no RBS env" with a single-line warning naming
|
|
153
|
+
# the offending file, rather than hanging.
|
|
151
154
|
#
|
|
152
155
|
# `bundle_path:` (String, optional): explicit path to the bundler install root (e.g.,
|
|
153
156
|
# "vendor/bundle" or an absolute path). Resolved relative to the project root (`paths:`'s base) when
|
|
@@ -326,7 +326,7 @@ module Rigor
|
|
|
326
326
|
payload = [
|
|
327
327
|
@labels_by_owner.sort.map { |owner, labels| [owner, labels.sort] },
|
|
328
328
|
@class_rows.keys.sort_by { |singleton| singleton ? 1 : 0 }
|
|
329
|
-
|
|
329
|
+
.map { |singleton| [singleton, sorted(@class_rows[singleton])] },
|
|
330
330
|
sorted(@path_rows), sorted(@self_rows), sorted(@result_rows),
|
|
331
331
|
@edges.map { |edge| [edge.target.to_s, edge.receiver, edge.selector.to_s, edge.plugin_id] }.sort,
|
|
332
332
|
@entry_points.map(&:to_h).sort_by { |preset| preset["name"] },
|
|
@@ -107,10 +107,9 @@ module Rigor
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def walk_namespace(node, prefix)
|
|
110
|
-
|
|
111
|
-
return node.rigor_each_child { |child| walk(child, prefix, false) } if
|
|
110
|
+
nested = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
111
|
+
return node.rigor_each_child { |child| walk(child, prefix, false) } if nested.nil?
|
|
112
112
|
|
|
113
|
-
nested = prefix + [name]
|
|
114
113
|
record_superclass(nested.join("::"), node, prefix) if node.is_a?(Prism::ClassNode)
|
|
115
114
|
walk(node.body, nested, false) if node.body
|
|
116
115
|
end
|
|
@@ -57,8 +57,12 @@ module Rigor
|
|
|
57
57
|
# When non-nil and non-empty, only `sig/` directories whose gem `(name, version, platform)` tuple
|
|
58
58
|
# matches a lockfile entry are returned. Bundle entries absent from the lockfile (or at a drifted
|
|
59
59
|
# version) are silently dropped — the lockfile is treated as the source of truth for "what gems this
|
|
60
|
-
# project actually declares".
|
|
61
|
-
#
|
|
60
|
+
# project actually declares". A git-sourced directory carries no version to compare (see
|
|
61
|
+
# {.gem_name_from_sig_path}), so for those the filter instead requires the matching `locked_gems`
|
|
62
|
+
# entry to have `git_source: true` — name alone is not enough, since a gem that moved off a `git:`
|
|
63
|
+
# source keeps its stale `bundler/gems/` directory under the SAME name until a `bundle clean`. Pass
|
|
64
|
+
# `nil` (the default) to keep the pre-Layer-3 behaviour of returning every non-skipped `sig/` under
|
|
65
|
+
# the bundle.
|
|
62
66
|
# @return [Array<Pathname>] every `<gem-dir>/sig` directory under the resolved bundle path, minus any
|
|
63
67
|
# whose gem name is in `skip_gems` and (when `locked_gems` is supplied) minus any whose `(name,
|
|
64
68
|
# version, platform)` does not match a lockfile entry.
|
|
@@ -72,14 +76,41 @@ module Rigor
|
|
|
72
76
|
)
|
|
73
77
|
return [] if resolved.nil?
|
|
74
78
|
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
|
|
79
|
+
# Two bundler-install layouts, both under the same `ruby/X.Y.Z/` root (`*` picks up whichever Ruby
|
|
80
|
+
# the bundle was installed for):
|
|
81
|
+
#
|
|
82
|
+
# - `gems/<name>-<ver>/sig/` — the RubyGems-sourced layout, the canonical case.
|
|
83
|
+
# - `bundler/gems/<repo>-<12-hex-revision>/sig/` — `git:`-sourced gems (`Bundler::Source::Git#
|
|
84
|
+
# install_path`); the directory name carries the git repository's basename and a 12-character
|
|
85
|
+
# revision prefix, not the gem name or version. Before this, a fork's own `sig/` was invisible to
|
|
86
|
+
# rigor no matter how it was authored — see issue #611.
|
|
87
|
+
#
|
|
88
|
+
# `path:`-sourced gems are NOT under either glob: Bundler does not copy them into the bundle root at
|
|
89
|
+
# all (`Bundler::Source::Path#path` is the user's own `path:` directory, used in place), so there is
|
|
90
|
+
# no bundle-relative location to walk. A `path:`-sourced gem's `sig/` is only reachable by adding it
|
|
91
|
+
# directly to the project's `signature_paths:` — which already works today and needs no discovery.
|
|
92
|
+
all = (Dir.glob(resolved.join("ruby", "*", "gems", "*", "sig")) +
|
|
93
|
+
Dir.glob(resolved.join("ruby", "*", "bundler", "gems", "*", "sig"))).map { |d| Pathname.new(d) }
|
|
78
94
|
filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
|
|
79
95
|
return filtered if locked_gems.nil? || locked_gems.empty?
|
|
80
96
|
|
|
81
97
|
expected_dirs = expected_gem_dirs(locked_gems)
|
|
82
|
-
filtered.select
|
|
98
|
+
filtered.select do |sig_dir|
|
|
99
|
+
if git_sourced_layout?(sig_dir)
|
|
100
|
+
# A git install's directory name carries no resolvable version (see above), so the lockfile
|
|
101
|
+
# filter can only check the gem NAME, not `(name, version)` — but name alone is not enough: a
|
|
102
|
+
# gem that used to be `git:`-sourced and has since moved to a released version keeps its old
|
|
103
|
+
# `bundler/gems/<repo>-<sha>/` directory sitting in the bundle tree until a `bundle clean`, and
|
|
104
|
+
# its NAME is still present in the lockfile (now via the rubygems entry). Matching on name only
|
|
105
|
+
# would silently readmit that stale git install — this repo's own `vendor/bundle` had exactly
|
|
106
|
+
# this after `binpacker` moved from `git:` to a released gem. `locked_gems` must say this name is
|
|
107
|
+
# CURRENTLY git-sourced, not merely present.
|
|
108
|
+
locked = locked_gems[gem_name_from_sig_path(sig_dir)]
|
|
109
|
+
!!locked&.git_source
|
|
110
|
+
else
|
|
111
|
+
expected_dirs.include?(sig_dir.parent.basename.to_s)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
83
114
|
end
|
|
84
115
|
|
|
85
116
|
# `{name => LockedGem}` → set of canonical bundler gem directory basenames. Pure-Ruby gems install as
|
|
@@ -98,19 +129,39 @@ module Rigor
|
|
|
98
129
|
end
|
|
99
130
|
private_class_method :expected_gem_dirs
|
|
100
131
|
|
|
101
|
-
# `<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig` → `<name
|
|
102
|
-
# `<
|
|
103
|
-
#
|
|
104
|
-
#
|
|
132
|
+
# `<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig` → `<name>`, or
|
|
133
|
+
# `<bundle>/ruby/X.Y.Z/bundler/gems/<repo>-<12-hex-revision>/sig` → `<repo>`. The two bundler-install
|
|
134
|
+
# layouts name their gem directory differently, so which suffix to strip depends on which layout
|
|
135
|
+
# `sig_dir` came from:
|
|
136
|
+
#
|
|
137
|
+
# - rubygems layout: `<name>-<version>` (platform-tagged variants like `ffi-1.17.4-aarch64-linux-gnu/`
|
|
138
|
+
# keep their platform suffix in the version part) — the version always starts with a digit, so strip
|
|
139
|
+
# from the first `-` followed by a digit.
|
|
140
|
+
# - git layout: `<repo-basename>-<12-hex-char-revision>` (`Bundler::Source::Git#install_path`) — no
|
|
141
|
+
# version at all, and the repository name need not equal the gem name (a fork hosted under a
|
|
142
|
+
# different repo name, a monorepo). Strip the fixed-width hex suffix instead; best-effort only, since
|
|
143
|
+
# the true gem name isn't recoverable from the filesystem without running Bundler.
|
|
105
144
|
#
|
|
106
145
|
# Public so the O4 Layer 3 slice-3 coverage report (`RbsCoverageReport`) can classify discovered bundle
|
|
107
146
|
# sigs against locked gem names without re-running discovery.
|
|
108
147
|
def self.gem_name_from_sig_path(sig_dir)
|
|
109
148
|
gem_dir = sig_dir.parent.basename.to_s
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
if git_sourced_layout?(sig_dir)
|
|
150
|
+
gem_dir.sub(/-[0-9a-f]{12}\z/, "")
|
|
151
|
+
else
|
|
152
|
+
gem_dir.sub(/-\d.*\z/, "")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# True when `sig_dir` sits under the `bundler/gems/` (git-sourced) layout rather than the plain
|
|
157
|
+
# `gems/` (rubygems-sourced) layout — `<bundle>/ruby/X.Y.Z/bundler/gems/<dir>/sig` vs.
|
|
158
|
+
# `<bundle>/ruby/X.Y.Z/gems/<dir>/sig`. A directory-shape check, not a name-shape guess: the two glob
|
|
159
|
+
# roots in {.discover} are already disjoint, so this just recovers which one a given path came from.
|
|
160
|
+
def self.git_sourced_layout?(sig_dir)
|
|
161
|
+
gem_dir = sig_dir.parent
|
|
162
|
+
gem_dir.parent.basename.to_s == "gems" && gem_dir.parent.parent.basename.to_s == "bundler"
|
|
113
163
|
end
|
|
164
|
+
private_class_method :git_sourced_layout?
|
|
114
165
|
|
|
115
166
|
# Returns `Pathname` resolved bundle path, or `nil` when neither explicit nor auto-detected. Public
|
|
116
167
|
# for the stats banner so end users can see what rigor picked up.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Environment
|
|
5
|
+
# Issue #784 — a mutable, first-write-wins record of one shared-build failure, held by the otherwise
|
|
6
|
+
# frozen {Environment} the way {HktRegistryHolder} holds a memoised value. A lazy build that raises
|
|
7
|
+
# during per-file analysis records here at its seam instead of raising into every file's
|
|
8
|
+
# `analyze_body` rescue; the coordinator snapshots the slot after the file loop (the build is lazy, so
|
|
9
|
+
# a snapshot taken before the loop reads nothing) and the aggregator surfaces it once for the run.
|
|
10
|
+
#
|
|
11
|
+
# First-write-wins: the build is memoised, so a second failure can only be the same one re-observed.
|
|
12
|
+
#
|
|
13
|
+
# Concurrency: single-threaded use only, the same discipline as {HktRegistryHolder}.
|
|
14
|
+
class FailureSlot
|
|
15
|
+
def initialize
|
|
16
|
+
@value = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param value [Array] a Marshal-clean tuple — the fork pool ships it back from the worker.
|
|
20
|
+
def record(value)
|
|
21
|
+
@value = value.freeze if @value.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Array, nil] the recorded tuple, or nil when the build never failed.
|
|
25
|
+
attr_reader :value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|