rigortype 0.1.18 → 0.2.0
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../scope"
|
|
6
|
+
require_relative "../inference/scope_indexer"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
# ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing
|
|
10
|
+
# harness (`tool/mutation/`, ADR-62). Only the *per-file effectiveness
|
|
11
|
+
# measurement* lives here — the type-visible {Mutator} and the warm-loop
|
|
12
|
+
# {MutationScanner} kill-rate measurement. The dev sweep / fuzz / survivor
|
|
13
|
+
# clustering stay off the frozen surface in `tool/mutation/mutate.rb`
|
|
14
|
+
# (which now reuses this {Mutator} so there is one source of truth).
|
|
15
|
+
module Protection
|
|
16
|
+
# One concrete edit: replace source bytes [start, stop) with `replacement`.
|
|
17
|
+
# `anchor` is the Prism node whose inferred type decides type-relevance —
|
|
18
|
+
# the call receiver whose contract the mutation could violate, or nil when
|
|
19
|
+
# there is no concrete receiver (implicit-self call, literal outside a call).
|
|
20
|
+
# `anchor_type` (the rendered receiver type) and `method_name` are filled in
|
|
21
|
+
# for reporting a surviving site; both may stay nil.
|
|
22
|
+
Mutation = Struct.new(
|
|
23
|
+
:operator, :expected_rule, :start, :stop, :replacement, :line, :label, :anchor,
|
|
24
|
+
:anchor_type, :method_name,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
def apply(source)
|
|
28
|
+
prefix = source.byteslice(0, start)
|
|
29
|
+
suffix = source.byteslice(stop, source.bytesize - stop) || ""
|
|
30
|
+
"#{prefix}#{replacement}#{suffix}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Generates type-visible mutations of a Ruby source string by walking the
|
|
35
|
+
# Prism AST and recording byte-range splices (no unparser needed — Prism
|
|
36
|
+
# hands us exact offsets, and the analyzer re-parses the spliced source).
|
|
37
|
+
#
|
|
38
|
+
# A mutation is "type-visible" when it should trip a diagnostic rule *if*
|
|
39
|
+
# Rigor holds a type at the site: a call-argument literal dropped to `nil`
|
|
40
|
+
# or type-swapped (→ `call.argument-type-mismatch`), or a call site renamed
|
|
41
|
+
# to a missing method (→ `call.undefined-method`). Only call sites and
|
|
42
|
+
# bodies are mutated, never `def` signatures, so a reused project scan stays
|
|
43
|
+
# valid.
|
|
44
|
+
class Mutator
|
|
45
|
+
IDENT = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
46
|
+
QUOTES = ['"', "'"].freeze
|
|
47
|
+
# Mutating an argument to a universal-equality method is always an
|
|
48
|
+
# equivalent mutant: Ruby's `==` / `<=>` family returns false / nil on a
|
|
49
|
+
# type mismatch rather than raising, so the engine exempts them
|
|
50
|
+
# (`UNIVERSAL_EQUALITY_METHODS`). Skip them to keep survivors meaningful.
|
|
51
|
+
UNIVERSAL_EQUALITY = %w[== != eql? equal? <=>].freeze
|
|
52
|
+
|
|
53
|
+
# Every operator the mutator knows. Each maps to the diagnostic rule
|
|
54
|
+
# family it is *engineered* to trip when the mutated value/call sits in a
|
|
55
|
+
# context where Rigor has type knowledge.
|
|
56
|
+
ALL_OPERATORS = %i[nil_inject type_swap undefined_method arity_extra].freeze
|
|
57
|
+
|
|
58
|
+
# The default set. `arity_extra` is excluded: most Ruby methods accept an
|
|
59
|
+
# extra argument (splat / optional), so appending one is usually an
|
|
60
|
+
# equivalent mutant — it contributes almost only noise. Re-enable it
|
|
61
|
+
# explicitly via `operators:` to measure arity teeth. (A signature-arity
|
|
62
|
+
# guard would make it default-worthy — a follow-up.)
|
|
63
|
+
OPERATORS = %i[nil_inject type_swap undefined_method].freeze
|
|
64
|
+
|
|
65
|
+
def initialize(source, operators: OPERATORS)
|
|
66
|
+
@source = source
|
|
67
|
+
@operators = operators
|
|
68
|
+
@parse = Prism.parse(source)
|
|
69
|
+
@anchor_for = {} # literal node -> its enclosing call's receiver node (or nil)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mutations
|
|
73
|
+
return [] unless @parse.success?
|
|
74
|
+
|
|
75
|
+
index_literal_anchors(@parse.value)
|
|
76
|
+
out = []
|
|
77
|
+
walk(@parse.value) { |node| collect(node, out) }
|
|
78
|
+
out
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Phase 1.5 — keep only mutations whose anchor types to a concrete,
|
|
82
|
+
# non-Dynamic type, i.e. a site where Rigor actually holds a contract the
|
|
83
|
+
# mutation could violate. Drops implicit-self calls and literals outside a
|
|
84
|
+
# typed call (no contract → guaranteed survival → noise). FP-safe
|
|
85
|
+
# direction: an unresolved/probe-failed type KEEPS the mutation, so the
|
|
86
|
+
# filter never hides a kill it is unsure about — it only removes
|
|
87
|
+
# provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope
|
|
88
|
+
# index from THIS mutator's parse so anchor node identity matches the keys.
|
|
89
|
+
def filter_by_type(mutations, environment:, path:)
|
|
90
|
+
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
91
|
+
index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
|
|
92
|
+
cache = {}
|
|
93
|
+
kept = mutations.select do |mut|
|
|
94
|
+
keep, type = anchor_decision(mut.anchor, index, cache)
|
|
95
|
+
mut.anchor_type = type if keep
|
|
96
|
+
keep
|
|
97
|
+
end
|
|
98
|
+
[kept, mutations.size - kept.size]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def walk(node, &blk)
|
|
104
|
+
return if node.nil?
|
|
105
|
+
|
|
106
|
+
blk.call(node)
|
|
107
|
+
node.compact_child_nodes.each { |child| walk(child, &blk) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def collect(node, out)
|
|
111
|
+
case node
|
|
112
|
+
when Prism::IntegerNode, Prism::FloatNode
|
|
113
|
+
literal_mutations(node, out, numeric: true)
|
|
114
|
+
when Prism::StringNode
|
|
115
|
+
literal_mutations(node, out, numeric: false)
|
|
116
|
+
when Prism::CallNode
|
|
117
|
+
call_mutations(node, out)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Record, for each literal that is a direct call argument, the receiver of
|
|
122
|
+
# the enclosing call — the anchor whose param contract a literal mutation
|
|
123
|
+
# could violate. Literals elsewhere get a nil anchor (filtered out under
|
|
124
|
+
# the type filter).
|
|
125
|
+
def index_literal_anchors(node)
|
|
126
|
+
return if node.nil?
|
|
127
|
+
|
|
128
|
+
if node.is_a?(Prism::CallNode) && node.arguments
|
|
129
|
+
node.arguments.arguments.each do |arg|
|
|
130
|
+
@anchor_for[arg] = [node.receiver, node.name.to_s] if literal?(arg)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
node.compact_child_nodes.each { |child| index_literal_anchors(child) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def literal?(node)
|
|
137
|
+
node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode) || node.is_a?(Prism::StringNode)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor
|
|
141
|
+
# holds a concrete (non-Dynamic/Top) type. FP-safe: an unresolved or
|
|
142
|
+
# probe-failed type keeps the mutation (with a nil rendered type).
|
|
143
|
+
def anchor_decision(anchor, index, cache)
|
|
144
|
+
return [false, nil] if anchor.nil?
|
|
145
|
+
return cache[anchor] if cache.key?(anchor)
|
|
146
|
+
|
|
147
|
+
cache[anchor] = compute_anchor_decision(anchor, index)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def compute_anchor_decision(anchor, index)
|
|
151
|
+
scope = index[anchor]
|
|
152
|
+
return [true, nil] if scope.nil? # unresolved scope → keep (FP-safe)
|
|
153
|
+
|
|
154
|
+
type = scope.type_of(anchor)
|
|
155
|
+
return [true, nil] if type.nil?
|
|
156
|
+
|
|
157
|
+
concrete = !non_concrete_type?(type)
|
|
158
|
+
[concrete, concrete ? render_type(type) : nil]
|
|
159
|
+
rescue StandardError
|
|
160
|
+
[true, nil] # never let a probe failure hide a candidate
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# A receiver type Rigor cannot bite on, so a mutation anchored to it would
|
|
164
|
+
# survive as noise: `Dynamic` / `Top` / `bot`, or a union with any such arm
|
|
165
|
+
# (gradually valid — `Array | Dynamic[top]`.whatever never fires). A union
|
|
166
|
+
# of fully-concrete arms (`String | Symbol`) stays concrete — it now has
|
|
167
|
+
# undefined-method teeth.
|
|
168
|
+
def non_concrete_type?(type)
|
|
169
|
+
return true if type.is_a?(Rigor::Type::Dynamic) || type.is_a?(Rigor::Type::Top) ||
|
|
170
|
+
type.is_a?(Rigor::Type::Bot)
|
|
171
|
+
return type.members.any? { |member| non_concrete_type?(member) } if type.is_a?(Rigor::Type::Union)
|
|
172
|
+
|
|
173
|
+
false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def render_type(type)
|
|
177
|
+
type.respond_to?(:describe) ? type.describe(:short) : type.to_s
|
|
178
|
+
rescue StandardError
|
|
179
|
+
type.class.name
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Mutate a literal: drop it to nil (possible-nil channel) and swap its
|
|
183
|
+
# type (type-mismatch channel). String literals are only touched when the
|
|
184
|
+
# node is a real quoted string, so we never corrupt `%w[...]` words.
|
|
185
|
+
def literal_mutations(node, out, numeric:)
|
|
186
|
+
return if !numeric && !QUOTES.include?(node.opening_loc&.slice)
|
|
187
|
+
|
|
188
|
+
anchor, method = @anchor_for[node]
|
|
189
|
+
return if UNIVERSAL_EQUALITY.include?(method)
|
|
190
|
+
|
|
191
|
+
loc = node.location
|
|
192
|
+
add(out, :nil_inject, "call.argument-type-mismatch", loc.start_offset, loc.end_offset,
|
|
193
|
+
"nil", loc.start_line, "literal → nil (#{snippet(loc)})", anchor, method)
|
|
194
|
+
swap = numeric ? '"rigor_mutant"' : "0"
|
|
195
|
+
add(out, :type_swap, "call.argument-type-mismatch", loc.start_offset, loc.end_offset,
|
|
196
|
+
swap, loc.start_line, "literal type swap (#{snippet(loc)} → #{swap})", anchor, method)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def call_mutations(node, out)
|
|
200
|
+
rename_call(node, out)
|
|
201
|
+
extend_arity(node, out)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Rename the *call site* (not the def) to a method that cannot exist, so a
|
|
205
|
+
# typed receiver trips call.undefined-method. We leave `def` signatures
|
|
206
|
+
# untouched on purpose: the prebuilt ProjectScan still carries the file's
|
|
207
|
+
# original declarations, so mutating only bodies/call-sites keeps it valid.
|
|
208
|
+
# Anchor is the explicit receiver (nil ⇒ implicit self ⇒ filtered out, as
|
|
209
|
+
# call.self-undefined-method ships `:off`).
|
|
210
|
+
def rename_call(node, out)
|
|
211
|
+
name = node.name.to_s
|
|
212
|
+
mloc = node.message_loc
|
|
213
|
+
return unless mloc && IDENT.match?(name)
|
|
214
|
+
|
|
215
|
+
add(out, :undefined_method, "call.undefined-method", mloc.start_offset, mloc.end_offset,
|
|
216
|
+
"#{name}__rigor_absent", mloc.start_line, "call ##{name} → missing method", node.receiver, name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Append a trailing argument inside explicit `(...)` parens to trip an
|
|
220
|
+
# arity diagnostic against a known fixed-arity signature.
|
|
221
|
+
def extend_arity(node, out)
|
|
222
|
+
open = node.opening_loc
|
|
223
|
+
close = node.closing_loc
|
|
224
|
+
return unless close && open&.slice == "("
|
|
225
|
+
|
|
226
|
+
args = node.arguments&.arguments
|
|
227
|
+
insertion = args && !args.empty? ? ", nil" : "nil"
|
|
228
|
+
add(out, :arity_extra, "call.wrong-arity", close.start_offset, close.start_offset,
|
|
229
|
+
insertion, node.location.start_line, "call ##{node.name} +1 arg", node.receiver, node.name.to_s)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def add(out, operator, rule, start, stop, replacement, line, label, anchor, method_name) # rubocop:disable Metrics/ParameterLists
|
|
233
|
+
return unless @operators.include?(operator)
|
|
234
|
+
|
|
235
|
+
out << Mutation.new(operator: operator, expected_rule: rule, start: start, stop: stop,
|
|
236
|
+
replacement: replacement, line: line, label: label, anchor: anchor,
|
|
237
|
+
method_name: method_name)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def snippet(loc)
|
|
241
|
+
text = loc.slice.gsub(/\s+/, " ")
|
|
242
|
+
text.length > 30 ? "#{text[0, 27]}..." : text
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -7,43 +7,30 @@ require_relative "rbs_extended/reporter"
|
|
|
7
7
|
require_relative "rbs_extended/hkt_directives"
|
|
8
8
|
|
|
9
9
|
module Rigor
|
|
10
|
-
#
|
|
11
|
-
# `RBS::Extended` annotation surface described in
|
|
10
|
+
# Reader for the `RBS::Extended` annotation surface described in
|
|
12
11
|
# `docs/type-specification/rbs-extended.md`.
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# consume. v0.0.2 recognises:
|
|
13
|
+
# Reads `%a{rigor:v1:<directive> <payload>}` annotations off RBS
|
|
14
|
+
# method definitions and returns well-typed effect objects the
|
|
15
|
+
# inference engine can consume. Implemented directives:
|
|
18
16
|
#
|
|
19
|
-
# - `rigor:v1:predicate-if-true <target> is <ClassName>`
|
|
20
|
-
# - `rigor:v1:predicate-if-false <target> is <ClassName>`
|
|
21
|
-
# - `rigor:v1:assert <target> is <ClassName>`
|
|
22
|
-
# - `rigor:v1:assert-if-true <target> is <ClassName>`
|
|
23
|
-
# - `rigor:v1:assert-if-false <target> is <ClassName>`
|
|
17
|
+
# - `rigor:v1:predicate-if-true <target> is <ClassName|refinement>`
|
|
18
|
+
# - `rigor:v1:predicate-if-false <target> is <ClassName|refinement>`
|
|
19
|
+
# - `rigor:v1:assert <target> is <ClassName|refinement>`
|
|
20
|
+
# - `rigor:v1:assert-if-true <target> is <ClassName|refinement>`
|
|
21
|
+
# - `rigor:v1:assert-if-false <target> is <ClassName|refinement>`
|
|
22
|
+
# - `rigor:v1:param <name> <type-expr>` — per-call param narrowing
|
|
23
|
+
# - `rigor:v1:return <type-expr>` — per-call return override
|
|
24
|
+
# - `rigor:v1:conforms-to <InterfaceName>` — structural conformance
|
|
24
25
|
#
|
|
25
|
-
# `predicate-if-*` fires when the call is used as an
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# `target: self` narrowing, ...) remain on the v0.0.x
|
|
34
|
-
# roadmap. Annotations whose key is in the `rigor:v1:`
|
|
35
|
-
# namespace but whose directive is unrecognised are
|
|
36
|
-
# silently ignored at first-preview quality (a future slice
|
|
37
|
-
# MAY surface them as diagnostics-on-Rigor-itself per the
|
|
38
|
-
# spec's "unsupported metadata" guidance).
|
|
39
|
-
#
|
|
40
|
-
# The parser is minimal: it accepts a strict shape
|
|
41
|
-
# `<target> is <ClassName>` where `<target>` is a Ruby
|
|
42
|
-
# identifier (parameter name) or `self`, and `<ClassName>`
|
|
43
|
-
# is a single non-namespaced class identifier or a
|
|
44
|
-
# `::Foo::Bar` style constant path. Negative refinements
|
|
45
|
-
# (`~T`), intersections, and unions are deferred to the
|
|
46
|
-
# next iteration.
|
|
26
|
+
# `predicate-if-*` fires when the call is used as an `if` / `unless`
|
|
27
|
+
# condition; `assert` fires unconditionally at the call's post-scope;
|
|
28
|
+
# `assert-if-true` / `assert-if-false` fire at the post-scope only
|
|
29
|
+
# when the call's return value can be observed as truthy / falsey.
|
|
30
|
+
# Negation (`~T`) is supported for both class-name and refinement
|
|
31
|
+
# right-hand sides. Parameterised refinements (`non-empty-array[T]`)
|
|
32
|
+
# are also recognised. Annotations whose directive is unrecognised
|
|
33
|
+
# are silently ignored per the spec's "unsupported metadata" guidance.
|
|
47
34
|
module RbsExtended # rubocop:disable Metrics/ModuleLength
|
|
48
35
|
DIRECTIVE_PREFIX = "rigor:v1:"
|
|
49
36
|
|
|
@@ -58,9 +45,10 @@ module Rigor
|
|
|
58
45
|
# a kebab-case refinement name (`non-empty-string`,
|
|
59
46
|
# `lowercase-string`, …) instead of a Capitalised class
|
|
60
47
|
# name. The narrowing tier substitutes the carrier for the
|
|
61
|
-
# current local type; `class_name` is then nil
|
|
62
|
-
#
|
|
63
|
-
#
|
|
48
|
+
# current local type; `class_name` is then nil. `negative`
|
|
49
|
+
# may be true for refinement-form directives — `~T` negation
|
|
50
|
+
# is supported; the narrowing tier computes the complement
|
|
51
|
+
# decomposition (see `AssertEffect` docs below).
|
|
64
52
|
class PredicateEffect < Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type)
|
|
65
53
|
def truthy_only? = edge == :truthy_only
|
|
66
54
|
def falsey_only? = edge == :falsey_only
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -17,11 +17,8 @@ module Rigor
|
|
|
17
17
|
# classes / modules, in-source constants, discovered method
|
|
18
18
|
# nodes, class ivar / cvar declarations).
|
|
19
19
|
#
|
|
20
|
-
# This module is the **stable read shape**
|
|
21
|
-
#
|
|
22
|
-
# calls out a unified reflection layer as a prerequisite for the
|
|
23
|
-
# extension protocols, and `docs/design/20260505-v0.1.0-readiness.md`
|
|
24
|
-
# nominates this module as the highest-leverage cold-start slice.
|
|
20
|
+
# This module is the **stable read shape** the plugin API is
|
|
21
|
+
# designed against (ADR-2, `docs/adr/2-extension-api.md`).
|
|
25
22
|
#
|
|
26
23
|
# The facade is **read-only and additive**. Existing call sites
|
|
27
24
|
# that read directly from `Rigor::Scope` or
|
|
@@ -52,8 +49,8 @@ module Rigor
|
|
|
52
49
|
# defined in the analyzed sources?
|
|
53
50
|
#
|
|
54
51
|
# The provenance side of the API (which source family contributed
|
|
55
|
-
# each fact) is explicitly out of scope for the v0.0.7 first
|
|
56
|
-
#
|
|
52
|
+
# each fact) is explicitly out of scope for the v0.0.7 first pass;
|
|
53
|
+
# v0.1.0's plugin API added it as a separate concern.
|
|
57
54
|
module Reflection
|
|
58
55
|
module_function
|
|
59
56
|
|
|
@@ -22,12 +22,15 @@ module Rigor
|
|
|
22
22
|
:in_source_constants,
|
|
23
23
|
:discovered_methods,
|
|
24
24
|
:discovered_def_nodes,
|
|
25
|
+
:discovered_singleton_def_nodes,
|
|
25
26
|
:discovered_def_sources,
|
|
26
27
|
:discovered_method_visibilities,
|
|
27
28
|
:discovered_superclasses,
|
|
28
29
|
:discovered_includes,
|
|
29
30
|
:discovered_class_sources,
|
|
30
|
-
:data_member_layouts
|
|
31
|
+
:data_member_layouts,
|
|
32
|
+
:struct_member_layouts,
|
|
33
|
+
:param_inferred_types
|
|
31
34
|
)
|
|
32
35
|
|
|
33
36
|
class DiscoveryIndex
|
|
@@ -46,12 +49,23 @@ module Rigor
|
|
|
46
49
|
in_source_constants: EMPTY_TABLE,
|
|
47
50
|
discovered_methods: EMPTY_TABLE,
|
|
48
51
|
discovered_def_nodes: EMPTY_TABLE,
|
|
52
|
+
discovered_singleton_def_nodes: EMPTY_TABLE,
|
|
49
53
|
discovered_def_sources: EMPTY_TABLE,
|
|
50
54
|
discovered_method_visibilities: EMPTY_TABLE,
|
|
51
55
|
discovered_superclasses: EMPTY_TABLE,
|
|
52
56
|
discovered_includes: EMPTY_TABLE,
|
|
53
57
|
discovered_class_sources: EMPTY_TABLE,
|
|
54
|
-
data_member_layouts: EMPTY_TABLE
|
|
58
|
+
data_member_layouts: EMPTY_TABLE,
|
|
59
|
+
struct_member_layouts: EMPTY_TABLE,
|
|
60
|
+
# ADR-67 WD3 — the call-site parameter-inference table, keyed by
|
|
61
|
+
# `[class_name, method_name, kind]` (the same `(class, method, kind)`
|
|
62
|
+
# triple {Inference::ParameterInferenceCollector} records and that
|
|
63
|
+
# `build_method_entry_scope` reconstructs from the lexical class path).
|
|
64
|
+
# The value is a `{param_name(Symbol) => Rigor::Type}` map of the union
|
|
65
|
+
# of resolved call-site argument types. Empty on every normal run; only
|
|
66
|
+
# the `coverage --protection` collection pass populates it today, so a
|
|
67
|
+
# `check` run leaves it empty and seeds nothing (byte-identical).
|
|
68
|
+
param_inferred_types: EMPTY_TABLE
|
|
55
69
|
)
|
|
56
70
|
end
|
|
57
71
|
end
|