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
|
@@ -18,7 +18,7 @@ module Rigor
|
|
|
18
18
|
# rendered receiver type) and `method_name` are filled in for reporting a surviving site; both may stay nil.
|
|
19
19
|
Mutation = Struct.new(
|
|
20
20
|
:operator, :expected_rule, :start, :stop, :replacement, :line, :label, :anchor,
|
|
21
|
-
:anchor_type, :method_name,
|
|
21
|
+
:anchor_type, :method_name, :call_node,
|
|
22
22
|
keyword_init: true
|
|
23
23
|
) do
|
|
24
24
|
def apply(source)
|
|
@@ -35,6 +35,103 @@ module Rigor
|
|
|
35
35
|
# call-argument literal dropped to `nil` or type-swapped (→ `call.argument-type-mismatch`), or a call site
|
|
36
36
|
# renamed to a missing method (→ `call.undefined-method`). Only call sites and bodies are mutated, never
|
|
37
37
|
# `def` signatures, so a reused project scan stays valid.
|
|
38
|
+
|
|
39
|
+
# Signature-arity guard for `arity_extra` (ADR-62): verifies through `ScopeIndexer` and
|
|
40
|
+
# `Reflection` that a call site targets a known, fixed/bounded positional signature and that
|
|
41
|
+
# adding 1 argument exceeds `max_arity`. Drops variadic or untyped calls to eliminate noise.
|
|
42
|
+
module ArityGuard
|
|
43
|
+
CONSTANT_CLASSES = {
|
|
44
|
+
Integer => "Integer", Float => "Float", String => "String",
|
|
45
|
+
Symbol => "Symbol", Range => "Range",
|
|
46
|
+
TrueClass => "TrueClass", FalseClass => "FalseClass",
|
|
47
|
+
NilClass => "NilClass"
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
def fixed_arity_call?(mut, index)
|
|
53
|
+
scope = call_scope(mut, index)
|
|
54
|
+
return false if scope.nil? || !plain_positional_call?(mut.call_node)
|
|
55
|
+
|
|
56
|
+
receiver_type = scope.type_of(mut.call_node.receiver)
|
|
57
|
+
class_name = concrete_class_name(receiver_type)
|
|
58
|
+
return false if class_name.nil? || !Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
59
|
+
|
|
60
|
+
max_arity = method_max_arity(receiver_type, class_name, mut.call_node.name, scope)
|
|
61
|
+
return false if max_arity.nil? || max_arity == Float::INFINITY
|
|
62
|
+
|
|
63
|
+
actual_args_count(mut.call_node) + 1 > max_arity
|
|
64
|
+
rescue StandardError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def call_scope(mut, index)
|
|
69
|
+
call_node = mut.call_node
|
|
70
|
+
return nil if call_node.nil? || call_node.receiver.nil?
|
|
71
|
+
|
|
72
|
+
index[call_node] || index[mut.anchor]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def actual_args_count(call_node)
|
|
76
|
+
(call_node.arguments&.arguments || []).size
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def method_max_arity(receiver_type, class_name, method_name, scope)
|
|
80
|
+
method_def =
|
|
81
|
+
if receiver_type.is_a?(Type::Singleton)
|
|
82
|
+
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
83
|
+
else
|
|
84
|
+
Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
85
|
+
end
|
|
86
|
+
return nil if method_def.nil? || method_def == true
|
|
87
|
+
|
|
88
|
+
max_positional_arity(method_def)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def plain_positional_call?(call_node)
|
|
92
|
+
arguments = call_node.arguments
|
|
93
|
+
return true if arguments.nil?
|
|
94
|
+
|
|
95
|
+
arguments.arguments.none? do |arg|
|
|
96
|
+
arg.is_a?(Prism::SplatNode) ||
|
|
97
|
+
arg.is_a?(Prism::KeywordHashNode) ||
|
|
98
|
+
arg.is_a?(Prism::BlockArgumentNode) ||
|
|
99
|
+
arg.is_a?(Prism::ForwardingArgumentsNode)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def max_positional_arity(method_def)
|
|
104
|
+
maxes = []
|
|
105
|
+
method_def.method_types.each do |mt|
|
|
106
|
+
function = mt.type
|
|
107
|
+
return nil unless function.respond_to?(:required_keywords)
|
|
108
|
+
return nil unless function.required_keywords.empty? && function.trailing_positionals.empty?
|
|
109
|
+
|
|
110
|
+
return Float::INFINITY if function.rest_positionals
|
|
111
|
+
|
|
112
|
+
min_arity = function.required_positionals.size
|
|
113
|
+
maxes << (min_arity + function.optional_positionals.size)
|
|
114
|
+
end
|
|
115
|
+
return nil if maxes.empty?
|
|
116
|
+
|
|
117
|
+
maxes.max
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def concrete_class_name(type)
|
|
121
|
+
case type
|
|
122
|
+
when Type::Nominal, Type::Singleton, Type::DataClass, Type::DataInstance,
|
|
123
|
+
Type::StructClass, Type::StructInstance
|
|
124
|
+
type.class_name
|
|
125
|
+
when Type::Tuple then "Array"
|
|
126
|
+
when Type::HashShape then "Hash"
|
|
127
|
+
when Type::Constant then CONSTANT_CLASSES[type.value.class] || type.value.class.name
|
|
128
|
+
when Type::Refined, Type::Difference then concrete_class_name(type.base)
|
|
129
|
+
when Type::IntegerRange then "Integer"
|
|
130
|
+
when Type::App then concrete_class_name(type.bound)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
38
135
|
class Mutator
|
|
39
136
|
IDENT = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
40
137
|
QUOTES = ['"', "'"].freeze
|
|
@@ -47,11 +144,9 @@ module Rigor
|
|
|
47
144
|
# the mutated value/call sits in a context where Rigor has type knowledge.
|
|
48
145
|
ALL_OPERATORS = %i[nil_inject type_swap undefined_method arity_extra].freeze
|
|
49
146
|
|
|
50
|
-
# The default set.
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
# default-worthy — a follow-up.)
|
|
54
|
-
OPERATORS = %i[nil_inject type_swap undefined_method].freeze
|
|
147
|
+
# The default set. With the signature-arity guard in place (`filter_by_type`), `arity_extra` is
|
|
148
|
+
# default-worthy: it only mutates call sites whose callee has a known, fixed/bounded positional arity.
|
|
149
|
+
OPERATORS = ALL_OPERATORS
|
|
55
150
|
|
|
56
151
|
def initialize(source, operators: OPERATORS)
|
|
57
152
|
@source = source
|
|
@@ -85,7 +180,9 @@ module Rigor
|
|
|
85
180
|
kept = mutations.select do |mut|
|
|
86
181
|
keep, type = anchor_decision(mut.anchor, index, cache)
|
|
87
182
|
mut.anchor_type = type if keep
|
|
88
|
-
keep
|
|
183
|
+
next false unless keep
|
|
184
|
+
|
|
185
|
+
mut.operator == :arity_extra ? ArityGuard.fixed_arity_call?(mut, index) : true
|
|
89
186
|
end
|
|
90
187
|
[kept, mutations.size - kept.size]
|
|
91
188
|
end
|
|
@@ -105,7 +202,7 @@ module Rigor
|
|
|
105
202
|
|
|
106
203
|
_keep, type = anchor_decision(mut.anchor, index, cache)
|
|
107
204
|
mut.anchor_type = type
|
|
108
|
-
true
|
|
205
|
+
mut.operator == :arity_extra ? ArityGuard.fixed_arity_call?(mut, index) : true
|
|
109
206
|
end
|
|
110
207
|
end
|
|
111
208
|
|
|
@@ -243,22 +340,28 @@ module Rigor
|
|
|
243
340
|
# Append a trailing argument inside explicit `(...)` parens to trip an arity diagnostic against a known
|
|
244
341
|
# fixed-arity signature.
|
|
245
342
|
def extend_arity(node, out)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
343
|
+
if node.closing_loc && node.opening_loc&.slice == "("
|
|
344
|
+
offset = node.closing_loc.start_offset
|
|
345
|
+
args = node.arguments&.arguments
|
|
346
|
+
insertion = args && !args.empty? ? ", nil" : "nil"
|
|
347
|
+
elsif node.arguments && !node.arguments.arguments.empty?
|
|
348
|
+
offset = node.arguments.location.end_offset
|
|
349
|
+
insertion = ", nil"
|
|
350
|
+
else
|
|
351
|
+
return
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
add(out, :arity_extra, "call.wrong-arity", offset, offset,
|
|
355
|
+
insertion, node.location.start_line, "call #{node.name} +1 arg", node.receiver,
|
|
356
|
+
node.name.to_s, call_node: node)
|
|
254
357
|
end
|
|
255
358
|
|
|
256
|
-
def add(out, operator, rule, start, stop, replacement, line, label, anchor, method_name) # rubocop:disable Metrics/ParameterLists
|
|
359
|
+
def add(out, operator, rule, start, stop, replacement, line, label, anchor, method_name, call_node: nil) # rubocop:disable Metrics/ParameterLists
|
|
257
360
|
return unless @operators.include?(operator)
|
|
258
361
|
|
|
259
362
|
out << Mutation.new(operator: operator, expected_rule: rule, start: start, stop: stop,
|
|
260
363
|
replacement: replacement, line: line, label: label, anchor: anchor,
|
|
261
|
-
method_name: method_name)
|
|
364
|
+
method_name: method_name, call_node: call_node)
|
|
262
365
|
end
|
|
263
366
|
|
|
264
367
|
def snippet(loc)
|
|
@@ -22,14 +22,15 @@ module Rigor
|
|
|
22
22
|
#
|
|
23
23
|
# ## Why it parses rather than reading the built environment
|
|
24
24
|
#
|
|
25
|
-
# {ConformanceChecker}, the obvious model, walks `RbsLoader`'s built env. An envelope
|
|
25
|
+
# {ConformanceChecker}, the obvious model, walks `RbsLoader`'s built env. An envelope did not: the
|
|
26
26
|
# diagnostic has to name **where the bound was written** (`sig/foo.rbs:12`), and the ADR-54 env
|
|
27
|
-
# cache
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
27
|
+
# cache reduced every `RBS::Location` to a zero-range `<cached>` sentinel, so a warm run lost both
|
|
28
|
+
# the position and the only evidence of which file a declaration came from. That particular loss is
|
|
29
|
+
# closed now — #696 restored the buffer name, #799 an annotation's position — so what still decides
|
|
30
|
+
# it is the rest: parsing the project's own signature sources is a few milliseconds over a tree
|
|
31
|
+
# Rigor already globs, it is identical warm and cold, and it enforces ADR-103 WD6's trust rule
|
|
32
|
+
# structurally: a `%a{pure}` in rbs core or in a gem's shipped RBS is never read, so it cannot bound
|
|
33
|
+
# a project method that happens to share its key.
|
|
33
34
|
#
|
|
34
35
|
# Malformed payloads and `pure`-versus-`effect` contradictions are recorded on a {Reporter} the
|
|
35
36
|
# scanner owns and ride out on {Result#unresolved}. They surface no diagnostic in this slice:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../inference/hkt_registry"
|
|
4
4
|
require_relative "../inference/hkt_body_parser"
|
|
5
|
+
require_relative "reporter"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module RbsExtended
|
|
@@ -286,10 +287,24 @@ module Rigor
|
|
|
286
287
|
source_location.respond_to?(:start_line) ? source_location.start_line : nil
|
|
287
288
|
end
|
|
288
289
|
|
|
290
|
+
# Issue #785 — routes a declined directive to the per-run reporter. The FIRST arm is the one that
|
|
291
|
+
# matters: the production reporter is {RbsExtended::Reporter}, whose surface is `record_unresolved` /
|
|
292
|
+
# `record_lossy_projection` / `record_hkt_error` — it has neither `#record` nor `#<<`, so before this
|
|
293
|
+
# arm existed every call here fell off the end of the `if` and every malformed HKT directive was
|
|
294
|
+
# dropped in every real run, while the doc comments claimed an `:info` entry was recorded. The
|
|
295
|
+
# position is flattened at record time through {RbsExtended::Reporter.position_of}, the one place
|
|
296
|
+
# every stream flattens through since #805, because the entry has to reach the pool drain channel as
|
|
297
|
+
# primitives (see {RbsExtended::Reporter}).
|
|
298
|
+
#
|
|
299
|
+
# The `#record` / `#<<` arms stay for the collecting doubles the directive specs and plugin authors
|
|
300
|
+
# use, which is the contract `scan_rbs_loader`'s `@param reporter [#record, nil]` documents.
|
|
289
301
|
def record_hkt_error(reporter, message, source_location)
|
|
290
302
|
return if reporter.nil?
|
|
291
303
|
|
|
292
|
-
if reporter.respond_to?(:
|
|
304
|
+
if reporter.respond_to?(:record_hkt_error)
|
|
305
|
+
path, line, column = Reporter.position_of(source_location)
|
|
306
|
+
reporter.record_hkt_error(message: message, path: path, line: line, column: column)
|
|
307
|
+
elsif reporter.respond_to?(:record)
|
|
293
308
|
reporter.record(directive: "hkt", message: message, source_location: source_location)
|
|
294
309
|
elsif reporter.respond_to?(:<<)
|
|
295
310
|
reporter << { directive: "hkt", message: message, source_location: source_location }
|
|
@@ -6,7 +6,7 @@ module Rigor
|
|
|
6
6
|
# cannot surface at the point of failure (the parsers are fail-soft, returning `nil` so call sites fall back
|
|
7
7
|
# to the RBS-declared type).
|
|
8
8
|
#
|
|
9
|
-
# Owns
|
|
9
|
+
# Owns three event streams:
|
|
10
10
|
#
|
|
11
11
|
# - `#unresolved_payloads` — `rigor:v1:*` directive payloads the resolver could not turn into a
|
|
12
12
|
# {Rigor::Type}. Surface as `dynamic.rbs-extended.unresolved` `:info` diagnostics.
|
|
@@ -14,21 +14,60 @@ module Rigor
|
|
|
14
14
|
# `required_of` / `readonly_of`) applied to a carrier that does not preserve shape information (anything
|
|
15
15
|
# other than `Type::HashShape` / `Type::Tuple`). Surface as `dynamic.shape.lossy-projection` `:info`
|
|
16
16
|
# diagnostics.
|
|
17
|
+
# - `#hkt_directive_errors` — malformed `rigor:v1:hkt_register` / `rigor:v1:hkt_define` directives the
|
|
18
|
+
# ADR-20 parser declined. Surface as `dynamic.rbs-extended.hkt-directive-invalid` `:info` diagnostics.
|
|
17
19
|
#
|
|
18
20
|
# Mutable through the run; consumed once by {Rigor::Analysis::Runner} at end-of-run. Each event is
|
|
19
|
-
# deduplicated by `(payload,
|
|
20
|
-
# lossy-projection
|
|
21
|
+
# deduplicated by its whole entry — `(payload, path, line, column)` for unresolved, `(head, path, line,
|
|
22
|
+
# column)` for lossy-projection, `(message, path, line, column)` for an hkt directive — so a single
|
|
23
|
+
# annotation read from many call sites yields one diagnostic.
|
|
21
24
|
#
|
|
22
25
|
# The reporter is intentionally thread-safe via a coarse `Mutex` because the inference engine may read the
|
|
23
26
|
# same method definition from multiple files in parallel; the critical sections are short (Array#include? +
|
|
24
27
|
# Array#<<) so the lock contention is negligible.
|
|
25
28
|
class Reporter
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
# Every entry carries its position as `(path, line, column)` primitives, flattened from the parser's
|
|
30
|
+
# `RBS::Location` by {.position_of} at record time. Issues #785 (hkt) and #805 (the two older streams).
|
|
31
|
+
# Two reasons, and either alone decides it. All three streams are drained out of every fork-pool worker
|
|
32
|
+
# ({Rigor::Analysis::WorkerSession#drain_reporters}), and an `RBS::Location` is a C-extension object with
|
|
33
|
+
# no `_dump`, so a `Marshal.dump` of the drain payload raises `TypeError` — which kills the worker at
|
|
34
|
+
# drain time and degrades the run to in-process re-analysis. And an `RBS::Location` compares equal only
|
|
35
|
+
# against a location over the SAME `RBS::Buffer` object, so two workers that each read the same `.rbs`
|
|
36
|
+
# would hand the coordinator two entries the dedup cannot collapse — `--workers=N` would then print N
|
|
37
|
+
# copies of a row `--workers=0` prints once.
|
|
38
|
+
#
|
|
39
|
+
# The record methods take the triple rather than the location for the same reason: a door that accepts
|
|
40
|
+
# an `RBS::Location` is a door a future caller reintroduces the bug through.
|
|
41
|
+
UnresolvedEntry = Data.define(:payload, :path, :line, :column)
|
|
42
|
+
LossyProjectionEntry = Data.define(:head, :path, :line, :column)
|
|
43
|
+
HktDirectiveEntry = Data.define(:message, :path, :line, :column)
|
|
44
|
+
|
|
45
|
+
# Flattens an `RBS::Location` (or anything answering the same readers) to the `(path, line, column)`
|
|
46
|
+
# triple every entry carries. `column` is 1-based, since `RBS::Location#start_column` is 0-based and
|
|
47
|
+
# diagnostics are not. Each component is `nil` when the location cannot supply it, and the diagnostic
|
|
48
|
+
# then falls back to `.rigor.yml:1:1`.
|
|
49
|
+
#
|
|
50
|
+
# Fail-soft by construction, like every parser that calls it: a location that raises while being read
|
|
51
|
+
# costs the entry its position, never the run.
|
|
52
|
+
#
|
|
53
|
+
# @param source_location [RBS::Location, nil]
|
|
54
|
+
# @return [Array(String, nil, Integer, nil, Integer, nil)]
|
|
55
|
+
def self.position_of(source_location)
|
|
56
|
+
return [nil, nil, nil] if source_location.nil?
|
|
57
|
+
|
|
58
|
+
buffer = source_location.respond_to?(:buffer) ? source_location.buffer : nil
|
|
59
|
+
name = buffer.respond_to?(:name) ? buffer.name.to_s : ""
|
|
60
|
+
line = source_location.respond_to?(:start_line) ? source_location.start_line : nil
|
|
61
|
+
column = source_location.respond_to?(:start_column) ? source_location.start_column + 1 : nil
|
|
62
|
+
[name.empty? ? nil : name, line, column]
|
|
63
|
+
rescue StandardError
|
|
64
|
+
[nil, nil, nil]
|
|
65
|
+
end
|
|
28
66
|
|
|
29
67
|
def initialize
|
|
30
68
|
@unresolved_payloads = []
|
|
31
69
|
@lossy_projections = []
|
|
70
|
+
@hkt_directive_errors = []
|
|
32
71
|
@mutex = Mutex.new
|
|
33
72
|
end
|
|
34
73
|
|
|
@@ -42,11 +81,13 @@ module Rigor
|
|
|
42
81
|
@mutex.synchronize { @lossy_projections.dup.freeze }
|
|
43
82
|
end
|
|
44
83
|
|
|
45
|
-
# Records a `dynamic.rbs-extended.unresolved` event. The
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
def record_unresolved(payload:,
|
|
49
|
-
entry = UnresolvedEntry.new(
|
|
84
|
+
# Records a `dynamic.rbs-extended.unresolved` event. The position triple is the source annotation's
|
|
85
|
+
# `.rbs` file / line / 1-based column — {.position_of} flattens the caller's `RBS::Location` into it —
|
|
86
|
+
# each `nil` when the caller had no location (the diagnostic then falls back to `.rigor.yml:1:1`).
|
|
87
|
+
def record_unresolved(payload:, path: nil, line: nil, column: nil)
|
|
88
|
+
entry = UnresolvedEntry.new(
|
|
89
|
+
payload: frozen_text(payload), path: frozen_text(path), line: line, column: column
|
|
90
|
+
)
|
|
50
91
|
@mutex.synchronize do
|
|
51
92
|
return if @unresolved_payloads.include?(entry)
|
|
52
93
|
|
|
@@ -56,8 +97,11 @@ module Rigor
|
|
|
56
97
|
|
|
57
98
|
# Records a `dynamic.shape.lossy-projection` event for one of the five shape-projection heads. `head` MUST
|
|
58
99
|
# be a String (`"pick_of"`, `"omit_of"`, …); the diagnostic message identifies which projection degraded.
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
# The position triple is read exactly as {#record_unresolved}'s is.
|
|
101
|
+
def record_lossy_projection(head:, path: nil, line: nil, column: nil)
|
|
102
|
+
entry = LossyProjectionEntry.new(
|
|
103
|
+
head: frozen_text(head), path: frozen_text(path), line: line, column: column
|
|
104
|
+
)
|
|
61
105
|
@mutex.synchronize do
|
|
62
106
|
return if @lossy_projections.include?(entry)
|
|
63
107
|
|
|
@@ -65,10 +109,46 @@ module Rigor
|
|
|
65
109
|
end
|
|
66
110
|
end
|
|
67
111
|
|
|
112
|
+
# @return [Array<HktDirectiveEntry>] frozen snapshot of the accumulated hkt-directive failures.
|
|
113
|
+
def hkt_directive_errors
|
|
114
|
+
@mutex.synchronize { @hkt_directive_errors.dup.freeze }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Records a `dynamic.rbs-extended.hkt-directive-invalid` event: one malformed ADR-20 HKT directive the
|
|
118
|
+
# {Rigor::RbsExtended::HktDirectives} parser declined. `message` names what the parser objected to; the
|
|
119
|
+
# position triple is the annotation's `.rbs` file / line / 1-based column, each `nil` when the caller had
|
|
120
|
+
# no location (the diagnostic then falls back to `.rigor.yml:1:1`).
|
|
121
|
+
#
|
|
122
|
+
# Every String is frozen individually, not just the enclosing `Data`: the entry crosses the pool drain
|
|
123
|
+
# channel, whose stated invariant is that its payload is `Ractor.shareable?` as well as Marshal-clean.
|
|
124
|
+
def record_hkt_error(message:, path: nil, line: nil, column: nil)
|
|
125
|
+
entry = HktDirectiveEntry.new(
|
|
126
|
+
message: frozen_text(message), path: frozen_text(path), line: line, column: column
|
|
127
|
+
)
|
|
128
|
+
@mutex.synchronize do
|
|
129
|
+
return if @hkt_directive_errors.include?(entry)
|
|
130
|
+
|
|
131
|
+
@hkt_directive_errors << entry
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
68
135
|
# True when no events have accumulated. Used by callers that want to skip the diagnostic-emission pass
|
|
69
136
|
# entirely on the common no-event path.
|
|
70
137
|
def empty?
|
|
71
|
-
@mutex.synchronize
|
|
138
|
+
@mutex.synchronize do
|
|
139
|
+
@unresolved_payloads.empty? && @lossy_projections.empty? && @hkt_directive_errors.empty?
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# `nil` stays `nil` — the diagnostic falls back to `.rigor.yml:1:1` on a missing path. Anything else
|
|
146
|
+
# becomes an individually frozen String: the `Data` wrapper is frozen on its own, but the pool drain's
|
|
147
|
+
# stated invariant is that its payload is `Ractor.shareable?`, which is a DEEP freeze. Coercing here also
|
|
148
|
+
# means a caller that hands the door an `RBS::Location` by mistake stores its `#to_s`, not the object —
|
|
149
|
+
# the entry cannot become un-Marshalable from the outside.
|
|
150
|
+
def frozen_text(value)
|
|
151
|
+
value.nil? ? nil : value.to_s.dup.freeze
|
|
72
152
|
end
|
|
73
153
|
end
|
|
74
154
|
end
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -812,10 +812,15 @@ module Rigor
|
|
|
812
812
|
# ADR-13 slice 3b — guards every reporter call so the in-RbsExtended-module call sites can record events
|
|
813
813
|
# uniformly without nil-checking each time. When the reporter is nil (the v0.1.0 → v0.1.3 default for call
|
|
814
814
|
# sites that do not yet thread `environment:`), the call is a no-op and the parser stays fail-soft.
|
|
815
|
+
#
|
|
816
|
+
# Issue #805 — the location is flattened to `(path, line, column)` here, at record time, because the
|
|
817
|
+
# entry is drained out of every fork-pool worker and an `RBS::Location` neither survives `Marshal.dump`
|
|
818
|
+
# nor compares equal across two workers' buffers (see {Rigor::RbsExtended::Reporter}).
|
|
815
819
|
def record_unresolved(reporter, payload, source_location)
|
|
816
820
|
return if reporter.nil?
|
|
817
821
|
|
|
818
|
-
|
|
822
|
+
path, line, column = Reporter.position_of(source_location)
|
|
823
|
+
reporter.record_unresolved(payload: payload, path: path, line: line, column: column)
|
|
819
824
|
end
|
|
820
825
|
end
|
|
821
826
|
end
|