rigortype 0.1.19 → 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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- 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/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -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 +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -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 +2 -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 +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- 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 +49 -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/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 +20 -28
- 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/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- 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/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- 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 +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- 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 +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- 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 +14 -2
- data/lib/rigor/scope.rb +54 -11
- 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/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -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 +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- 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 +7 -9
- 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 +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- 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 +1 -1
- 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 +5 -5
- 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 +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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 +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -32,9 +32,9 @@ module Rigor
|
|
|
32
32
|
# its `#prepare(services)` hook. `consumes:` lists the
|
|
33
33
|
# `(plugin_id, name)` pairs this plugin reads from
|
|
34
34
|
# `services.fact_store`. The loader uses both for
|
|
35
|
-
# topological sort + missing-producer detection
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
# topological sort + missing-producer detection; slice 4
|
|
36
|
+
# added the declarations; slice 5 (`Loader#topo_sort_plugins`)
|
|
37
|
+
# enforces ordering and missing-producer validation.
|
|
38
38
|
class Consumption < Data.define(:plugin_id, :name, :optional)
|
|
39
39
|
def initialize(plugin_id:, name:, optional: false)
|
|
40
40
|
super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
|
|
@@ -277,10 +277,18 @@ module Rigor
|
|
|
277
277
|
end
|
|
278
278
|
end
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
# Shared shape check for the Array-of-X manifest fields. Every entry
|
|
281
|
+
# must satisfy the block; otherwise raise the uniform "must be an
|
|
282
|
+
# Array of <label>" message. Centralises the message format the
|
|
283
|
+
# field validators below share so it cannot drift between them.
|
|
284
|
+
def validate_array_of!(field, value, label, &)
|
|
285
|
+
return if value.is_a?(Array) && value.all?(&)
|
|
282
286
|
|
|
283
|
-
raise ArgumentError, "plugin manifest
|
|
287
|
+
raise ArgumentError, "plugin manifest #{field} must be an Array of #{label}, got #{value.inspect}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def validate_produces!(produces)
|
|
291
|
+
validate_array_of!("produces", produces, "Symbol/String") { |p| p.is_a?(Symbol) || p.is_a?(String) }
|
|
284
292
|
end
|
|
285
293
|
|
|
286
294
|
# ADR-10 5a — `owns_receivers:` declares the class names
|
|
@@ -291,11 +299,7 @@ module Rigor
|
|
|
291
299
|
# so plugin contributions stay authoritative for those
|
|
292
300
|
# types.
|
|
293
301
|
def validate_owns_receivers!(owns_receivers)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
raise ArgumentError,
|
|
297
|
-
"plugin manifest owns_receivers must be an Array of non-empty String, " \
|
|
298
|
-
"got #{owns_receivers.inspect}"
|
|
302
|
+
validate_array_of!("owns_receivers", owns_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
|
|
299
303
|
end
|
|
300
304
|
|
|
301
305
|
# ADR-26 — `open_receivers:` declares the class names this
|
|
@@ -309,11 +313,7 @@ module Rigor
|
|
|
309
313
|
# `owns_receivers:` (which routes dispatch); this one only
|
|
310
314
|
# suppresses the diagnostic.
|
|
311
315
|
def validate_open_receivers!(open_receivers)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
raise ArgumentError,
|
|
315
|
-
"plugin manifest open_receivers must be an Array of non-empty String, " \
|
|
316
|
-
"got #{open_receivers.inspect}"
|
|
316
|
+
validate_array_of!("open_receivers", open_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
|
|
317
317
|
end
|
|
318
318
|
|
|
319
319
|
# ADR-13 slice 2 — `type_node_resolvers:` declares the
|
|
@@ -325,11 +325,9 @@ module Rigor
|
|
|
325
325
|
# integration that actually drives the chain lands in
|
|
326
326
|
# slice 3.
|
|
327
327
|
def validate_type_node_resolvers!(resolvers)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"plugin manifest type_node_resolvers must be an Array of " \
|
|
332
|
-
"Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
|
|
328
|
+
validate_array_of!("type_node_resolvers", resolvers, "Rigor::Plugin::TypeNodeResolver instances") do |r|
|
|
329
|
+
r.is_a?(TypeNodeResolver)
|
|
330
|
+
end
|
|
333
331
|
end
|
|
334
332
|
|
|
335
333
|
# ADR-16 slice 1a — `block_as_methods:` declares the Tier A
|
|
@@ -338,11 +336,9 @@ module Rigor
|
|
|
338
336
|
# actually narrows `Scope#self_type` for matching blocks
|
|
339
337
|
# arrives in a subsequent slice.
|
|
340
338
|
def validate_block_as_methods!(entries)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
"plugin manifest block_as_methods must be an Array of " \
|
|
345
|
-
"Rigor::Plugin::Macro::BlockAsMethod instances, got #{entries.inspect}"
|
|
339
|
+
validate_array_of!("block_as_methods", entries, "Rigor::Plugin::Macro::BlockAsMethod instances") do |e|
|
|
340
|
+
e.is_a?(Macro::BlockAsMethod)
|
|
341
|
+
end
|
|
346
342
|
end
|
|
347
343
|
|
|
348
344
|
# ADR-16 slice 2a — `heredoc_templates:` declares the Tier C
|
|
@@ -351,11 +347,9 @@ module Rigor
|
|
|
351
347
|
# manifest; the pre-pass + `SyntheticMethodIndex` that actually
|
|
352
348
|
# emit synthetic methods arrive in slice 2b.
|
|
353
349
|
def validate_heredoc_templates!(entries)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
"plugin manifest heredoc_templates must be an Array of " \
|
|
358
|
-
"Rigor::Plugin::Macro::HeredocTemplate instances, got #{entries.inspect}"
|
|
350
|
+
validate_array_of!("heredoc_templates", entries, "Rigor::Plugin::Macro::HeredocTemplate instances") do |e|
|
|
351
|
+
e.is_a?(Macro::HeredocTemplate)
|
|
352
|
+
end
|
|
359
353
|
end
|
|
360
354
|
|
|
361
355
|
# ADR-36 — `nested_class_templates:` declares the
|
|
@@ -365,11 +359,10 @@ module Rigor
|
|
|
365
359
|
# subclasses + their `#inner` reader through the existing
|
|
366
360
|
# `SyntheticMethodIndex` primitive.
|
|
367
361
|
def validate_nested_class_templates!(entries)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"Rigor::Plugin::Macro::NestedClassTemplate instances, got #{entries.inspect}"
|
|
362
|
+
validate_array_of!("nested_class_templates", entries,
|
|
363
|
+
"Rigor::Plugin::Macro::NestedClassTemplate instances") do |e|
|
|
364
|
+
e.is_a?(Macro::NestedClassTemplate)
|
|
365
|
+
end
|
|
373
366
|
end
|
|
374
367
|
|
|
375
368
|
# ADR-16 slice 3a — `trait_registries:` declares the Tier B
|
|
@@ -379,11 +372,9 @@ module Rigor
|
|
|
379
372
|
# `SyntheticMethodIndex` (slice 2b primitive) arrives in
|
|
380
373
|
# slice 3b.
|
|
381
374
|
def validate_trait_registries!(entries)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
"plugin manifest trait_registries must be an Array of " \
|
|
386
|
-
"Rigor::Plugin::Macro::TraitRegistry instances, got #{entries.inspect}"
|
|
375
|
+
validate_array_of!("trait_registries", entries, "Rigor::Plugin::Macro::TraitRegistry instances") do |e|
|
|
376
|
+
e.is_a?(Macro::TraitRegistry)
|
|
377
|
+
end
|
|
387
378
|
end
|
|
388
379
|
|
|
389
380
|
# ADR-20 slice 6 — `hkt_registrations:` declares the
|
|
@@ -398,11 +389,9 @@ module Rigor
|
|
|
398
389
|
# user `.rbs` overlays merge on top of plugin entries
|
|
399
390
|
# last-write-wins.
|
|
400
391
|
def validate_hkt_registrations!(entries)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
"plugin manifest hkt_registrations must be an Array of " \
|
|
405
|
-
"Rigor::Inference::HktRegistry::Registration instances, got #{entries.inspect}"
|
|
392
|
+
validate_array_of!("hkt_registrations", entries, "Rigor::Inference::HktRegistry::Registration instances") do |e|
|
|
393
|
+
e.is_a?(Inference::HktRegistry::Registration)
|
|
394
|
+
end
|
|
406
395
|
end
|
|
407
396
|
|
|
408
397
|
# ADR-20 slice 6 — `hkt_definitions:` declares the
|
|
@@ -415,11 +404,9 @@ module Rigor
|
|
|
415
404
|
# via {Rigor::Inference::HktBody}'s node-constructor API
|
|
416
405
|
# without parsing a string.
|
|
417
406
|
def validate_hkt_definitions!(entries)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"plugin manifest hkt_definitions must be an Array of " \
|
|
422
|
-
"Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
|
|
407
|
+
validate_array_of!("hkt_definitions", entries, "Rigor::Inference::HktRegistry::Definition instances") do |e|
|
|
408
|
+
e.is_a?(Inference::HktRegistry::Definition)
|
|
409
|
+
end
|
|
423
410
|
end
|
|
424
411
|
|
|
425
412
|
# ADR-25 — `signature_paths:` declares the RBS signature
|
|
@@ -429,11 +416,7 @@ module Rigor
|
|
|
429
416
|
# loader validates each exists and `Environment.for_project`
|
|
430
417
|
# merges the resolved set into the RBS environment.
|
|
431
418
|
def validate_signature_paths!(paths)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
raise ArgumentError,
|
|
435
|
-
"plugin manifest signature_paths must be an Array of non-empty String, " \
|
|
436
|
-
"got #{paths.inspect}"
|
|
419
|
+
validate_array_of!("signature_paths", paths, "non-empty String") { |p| p.is_a?(String) && !p.empty? }
|
|
437
420
|
end
|
|
438
421
|
|
|
439
422
|
# ADR-28 — `protocol_contracts:` declares the path-scoped
|
|
@@ -449,11 +432,9 @@ module Rigor
|
|
|
449
432
|
# MAY override `Plugin::Base#protocol_contracts` to fold in
|
|
450
433
|
# per-project config (e.g. a custom convention path).
|
|
451
434
|
def validate_protocol_contracts!(entries)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"plugin manifest protocol_contracts must be an Array of " \
|
|
456
|
-
"Rigor::Plugin::ProtocolContract instances, got #{entries.inspect}"
|
|
435
|
+
validate_array_of!("protocol_contracts", entries, "Rigor::Plugin::ProtocolContract instances") do |e|
|
|
436
|
+
e.is_a?(ProtocolContract)
|
|
437
|
+
end
|
|
457
438
|
end
|
|
458
439
|
|
|
459
440
|
# ADR-38 — `additional_initializers:` declares the
|
|
@@ -465,11 +446,9 @@ module Rigor
|
|
|
465
446
|
# loaded plugins; `Inference::ScopeIndexer` consults the set at
|
|
466
447
|
# its single gate.
|
|
467
448
|
def validate_additional_initializers!(entries)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"plugin manifest additional_initializers must be an Array of " \
|
|
472
|
-
"Rigor::Plugin::AdditionalInitializer instances, got #{entries.inspect}"
|
|
449
|
+
validate_array_of!("additional_initializers", entries, "Rigor::Plugin::AdditionalInitializer instances") do |e|
|
|
450
|
+
e.is_a?(AdditionalInitializer)
|
|
451
|
+
end
|
|
473
452
|
end
|
|
474
453
|
|
|
475
454
|
# ADR-32 WD4 — `source_rbs_synthesizer:` declares a callable
|
|
@@ -317,13 +317,12 @@ module Rigor
|
|
|
317
317
|
!load_errors.empty?
|
|
318
318
|
end
|
|
319
319
|
|
|
320
|
-
# ADR-13
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
# — registration order is the user's lever.
|
|
320
|
+
# ADR-13 — flat ordered list of every loaded plugin's
|
|
321
|
+
# manifest-declared {TypeNodeResolver} instances, in plugin
|
|
322
|
+
# registration order. `Environment#build_name_scope` builds a
|
|
323
|
+
# `TypeNode::ResolverChain` from this list (environment.rb).
|
|
324
|
+
# The first non-nil `#resolve(node, scope)` return wins per
|
|
325
|
+
# ADR-13 WD3 / WD5 — registration order is the user's lever.
|
|
327
326
|
def type_node_resolvers
|
|
328
327
|
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
329
328
|
end
|
|
@@ -24,14 +24,12 @@ module Rigor
|
|
|
24
24
|
# )
|
|
25
25
|
# end
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# in isolation but never run for a real `%a{rigor:v1:...}`
|
|
34
|
-
# payload.
|
|
27
|
+
# ADR-13 — base class, manifest hook, and registry aggregation
|
|
28
|
+
# for plugin-contributed type-node resolvers. Resolvers declared
|
|
29
|
+
# via `manifest(type_node_resolvers:)` run for every real
|
|
30
|
+
# `%a{rigor:v1:...}` payload through `TypeNode::ResolverChain`
|
|
31
|
+
# (built by `Environment#build_name_scope` from
|
|
32
|
+
# `Plugin::Registry#type_node_resolvers`).
|
|
35
33
|
#
|
|
36
34
|
# Resolvers SHOULD be stateless and re-entrant; the registry
|
|
37
35
|
# builds the chain once per `Analysis::Runner.run` and may
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../analysis/runner"
|
|
6
|
+
require_relative "mutator"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Protection
|
|
10
|
+
# ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind
|
|
11
|
+
# Tier 1's static {Inference::ProtectionScanner} proxy). For one file it
|
|
12
|
+
# answers the question Tier 1 only bounds: when a type-visible bug is
|
|
13
|
+
# introduced at a dispatch site, does Rigor actually catch it?
|
|
14
|
+
#
|
|
15
|
+
# Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
|
|
16
|
+
# generate the type-visible mutations ({Mutator}), keep only those whose
|
|
17
|
+
# receiver Rigor holds a concrete type for (the type-aware filter — the
|
|
18
|
+
# FP-safe meaning-maker; an unresolved receiver is kept), then for each:
|
|
19
|
+
# re-analyse the mutated SOURCE against a clean baseline and read whether a
|
|
20
|
+
# NEW diagnostic appears. A *killed* mutation is a caught breakage; a
|
|
21
|
+
# *survived* one is a breakage Rigor missed — an "add a type here" site.
|
|
22
|
+
#
|
|
23
|
+
# The expensive builds (RBS environment + the whole-project pre-pass scan)
|
|
24
|
+
# are paid ONCE by the caller and threaded in via `environment:` /
|
|
25
|
+
# `project_scan:`; each mutant reuses them through
|
|
26
|
+
# `Runner.new(prebuilt:)#run_source` (in-memory overlay, no disk write).
|
|
27
|
+
# Passing `prebuilt:` disables the run-result cache (whose key digests the
|
|
28
|
+
# *disk* file), so a mutant is never served a stale clean hit.
|
|
29
|
+
class MutationScanner
|
|
30
|
+
# A surviving mutation site — a breakage Rigor did not catch.
|
|
31
|
+
SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
|
|
32
|
+
|
|
33
|
+
FileResult = Data.define(:path, :killed, :survived, :sites) do
|
|
34
|
+
# Mutations actually analysed (parse-invalid mutants are not counted).
|
|
35
|
+
def total = killed + survived
|
|
36
|
+
|
|
37
|
+
# Effectiveness ratio; a file with no type-relevant mutation is
|
|
38
|
+
# vacuously fully effective (no breakage was available to miss).
|
|
39
|
+
def ratio = total.zero? ? 1.0 : killed.to_f / total
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param configuration [Rigor::Configuration]
|
|
43
|
+
# @param environment [Rigor::Environment] pre-built once by the caller
|
|
44
|
+
# @param project_scan [Rigor::Analysis::ProjectScan] pre-built once
|
|
45
|
+
# @param limit [Integer, nil] optional per-file mutation cap (sampled with
|
|
46
|
+
# `seed`); nil analyses every type-relevant mutation (deterministic).
|
|
47
|
+
# @param seed [Integer] RNG seed for the optional sample.
|
|
48
|
+
def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1)
|
|
49
|
+
@configuration = configuration
|
|
50
|
+
@environment = environment
|
|
51
|
+
@project_scan = project_scan
|
|
52
|
+
@limit = limit
|
|
53
|
+
@seed = seed
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param path [String] the file to measure (used as the in-memory bind path)
|
|
57
|
+
# @param source [String, nil] the file's source; read from disk when nil
|
|
58
|
+
# @return [FileResult]
|
|
59
|
+
def scan_file(path, source: nil)
|
|
60
|
+
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
61
|
+
mutator = Mutator.new(source)
|
|
62
|
+
kept, = mutator.filter_by_type(mutator.mutations, environment: @environment, path: path)
|
|
63
|
+
kept = sample(kept)
|
|
64
|
+
return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
|
|
65
|
+
|
|
66
|
+
baseline = signatures(analyse(source, path))
|
|
67
|
+
measure(source, path, kept, baseline)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def measure(source, path, mutations, baseline)
|
|
73
|
+
killed = 0
|
|
74
|
+
sites = []
|
|
75
|
+
mutations.each do |mut|
|
|
76
|
+
case classify(source, path, mut, baseline)
|
|
77
|
+
when :killed then killed += 1
|
|
78
|
+
when :survived then sites << surviving_site(mut)
|
|
79
|
+
# :invalid — a parse-broken mutant; not a measurement, skip it.
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def classify(source, path, mut, baseline)
|
|
86
|
+
mutant_source = mut.apply(source)
|
|
87
|
+
return :invalid unless Prism.parse(mutant_source).success?
|
|
88
|
+
|
|
89
|
+
new_diagnostics = analyse(mutant_source, path).reject { |d| baseline.include?(sig(d)) }
|
|
90
|
+
new_diagnostics.empty? ? :survived : :killed
|
|
91
|
+
rescue StandardError
|
|
92
|
+
# A harness-level failure on one mutant must not abort the file.
|
|
93
|
+
:invalid
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# cache_store: nil + prebuilt: scan ⇒ the run cache is bypassed and the
|
|
97
|
+
# mutant is always re-analysed against the in-memory bytes.
|
|
98
|
+
def analyse(source, path)
|
|
99
|
+
Rigor::Analysis::Runner.new(
|
|
100
|
+
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
101
|
+
cache_store: nil, collect_stats: false
|
|
102
|
+
).run_source(source: source, path: path).diagnostics
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def sample(mutations)
|
|
106
|
+
return mutations unless @limit
|
|
107
|
+
|
|
108
|
+
mutations.sample(@limit, random: Random.new(@seed))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def signatures(diagnostics) = diagnostics.to_set { |d| sig(d) }
|
|
112
|
+
def sig(diagnostic) = [diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
|
|
113
|
+
|
|
114
|
+
def surviving_site(mut)
|
|
115
|
+
SurvivingSite.new(line: mut.line, receiver: mut.anchor_type,
|
|
116
|
+
method_name: mut.method_name, operator: mut.operator.to_s)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -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
|