kimera 0.1.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.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/.kimera.yml.example +59 -0
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +528 -0
  6. data/exe/kimera +22 -0
  7. data/lib/kimera/audit/operator_audit.rb +71 -0
  8. data/lib/kimera/cli/argv.rb +15 -0
  9. data/lib/kimera/cli/baseline.rb +87 -0
  10. data/lib/kimera/cli/changed.rb +27 -0
  11. data/lib/kimera/cli/ci.rb +27 -0
  12. data/lib/kimera/cli/completion.rb +37 -0
  13. data/lib/kimera/cli/doctor.rb +115 -0
  14. data/lib/kimera/cli/flag.rb +55 -0
  15. data/lib/kimera/cli/help_text.rb +45 -0
  16. data/lib/kimera/cli/init.rb +105 -0
  17. data/lib/kimera/cli/mutant.rb +82 -0
  18. data/lib/kimera/cli/report_file.rb +20 -0
  19. data/lib/kimera/cli/run/arguments.rb +58 -0
  20. data/lib/kimera/cli/run/cycle.rb +78 -0
  21. data/lib/kimera/cli/run/digest.rb +49 -0
  22. data/lib/kimera/cli/run/emission.rb +45 -0
  23. data/lib/kimera/cli/run/gate.rb +43 -0
  24. data/lib/kimera/cli/run/options.rb +85 -0
  25. data/lib/kimera/cli/run/pass.rb +100 -0
  26. data/lib/kimera/cli/run/sources.rb +56 -0
  27. data/lib/kimera/cli/run.rb +52 -0
  28. data/lib/kimera/cli/skill.rb +23 -0
  29. data/lib/kimera/cli/suggestion.rb +36 -0
  30. data/lib/kimera/cli/survivors/panel.rb +81 -0
  31. data/lib/kimera/cli/survivors.rb +89 -0
  32. data/lib/kimera/cli/synthesize.rb +66 -0
  33. data/lib/kimera/cli/workflows.rb +8 -0
  34. data/lib/kimera/cli.rb +132 -0
  35. data/lib/kimera/error.rb +3 -0
  36. data/lib/kimera/execution/baseline_failure.rb +47 -0
  37. data/lib/kimera/execution/baseline_pass.rb +123 -0
  38. data/lib/kimera/execution/boot.rb +50 -0
  39. data/lib/kimera/execution/callback.rb +33 -0
  40. data/lib/kimera/execution/child_process.rb +48 -0
  41. data/lib/kimera/execution/composite_isolation.rb +24 -0
  42. data/lib/kimera/execution/harness.rb +150 -0
  43. data/lib/kimera/execution/isolated.rb +64 -0
  44. data/lib/kimera/execution/isolated_child.rb +15 -0
  45. data/lib/kimera/execution/isolated_child_command.rb +44 -0
  46. data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
  47. data/lib/kimera/execution/isolated_plan.rb +54 -0
  48. data/lib/kimera/execution/isolated_scheduling.rb +54 -0
  49. data/lib/kimera/execution/isolated_test_selection.rb +22 -0
  50. data/lib/kimera/execution/isolated_verdict.rb +72 -0
  51. data/lib/kimera/execution/isolated_watchdog.rb +59 -0
  52. data/lib/kimera/execution/isolation.rb +17 -0
  53. data/lib/kimera/execution/null_progress.rb +17 -0
  54. data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
  55. data/lib/kimera/execution/overlay_guards.rb +56 -0
  56. data/lib/kimera/execution/parallel_test_databases.rb +64 -0
  57. data/lib/kimera/execution/pool_driver.rb +101 -0
  58. data/lib/kimera/execution/priority.rb +23 -0
  59. data/lib/kimera/execution/reload.rb +104 -0
  60. data/lib/kimera/execution/rig.rb +41 -0
  61. data/lib/kimera/execution/schedule.rb +105 -0
  62. data/lib/kimera/execution/schemata.rb +88 -0
  63. data/lib/kimera/execution/shift/attempt.rb +34 -0
  64. data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
  65. data/lib/kimera/execution/shift/killer_memory.rb +20 -0
  66. data/lib/kimera/execution/shift/leak_guard.rb +34 -0
  67. data/lib/kimera/execution/shift.rb +137 -0
  68. data/lib/kimera/execution/sweep.rb +30 -0
  69. data/lib/kimera/execution/transaction_isolation.rb +32 -0
  70. data/lib/kimera/execution/trial.rb +16 -0
  71. data/lib/kimera/execution/verdicts.rb +78 -0
  72. data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
  73. data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
  74. data/lib/kimera/execution/worker_pool/worker.rb +32 -0
  75. data/lib/kimera/execution/worker_pool.rb +101 -0
  76. data/lib/kimera/frameworks/adapter.rb +82 -0
  77. data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
  78. data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
  79. data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
  80. data/lib/kimera/incremental/git_diff.rb +98 -0
  81. data/lib/kimera/incremental/selection.rb +27 -0
  82. data/lib/kimera/incremental/session.rb +109 -0
  83. data/lib/kimera/memoization.rb +104 -0
  84. data/lib/kimera/operators/argument_drop.rb +19 -0
  85. data/lib/kimera/operators/arithmetic.rb +12 -0
  86. data/lib/kimera/operators/audit.rb +3 -0
  87. data/lib/kimera/operators/base.rb +40 -0
  88. data/lib/kimera/operators/binary_swap.rb +23 -0
  89. data/lib/kimera/operators/boolean.rb +26 -0
  90. data/lib/kimera/operators/chain_link_deletion.rb +25 -0
  91. data/lib/kimera/operators/collection_literal.rb +18 -0
  92. data/lib/kimera/operators/comparison.rb +13 -0
  93. data/lib/kimera/operators/conditional.rb +36 -0
  94. data/lib/kimera/operators/default_argument.rb +17 -0
  95. data/lib/kimera/operators/element_drop.rb +31 -0
  96. data/lib/kimera/operators/index_fetch.rb +15 -0
  97. data/lib/kimera/operators/kernel_coercion.rb +17 -0
  98. data/lib/kimera/operators/method_unwrap.rb +20 -0
  99. data/lib/kimera/operators/negation.rb +15 -0
  100. data/lib/kimera/operators/node_swap.rb +20 -0
  101. data/lib/kimera/operators/numeric_literal.rb +39 -0
  102. data/lib/kimera/operators/op_assign.rb +18 -0
  103. data/lib/kimera/operators/plugins.rb +27 -0
  104. data/lib/kimera/operators/rails_association.rb +24 -0
  105. data/lib/kimera/operators/rails_callback.rb +49 -0
  106. data/lib/kimera/operators/rails_declaration.rb +16 -0
  107. data/lib/kimera/operators/rails_permit.rb +34 -0
  108. data/lib/kimera/operators/rails_validation.rb +17 -0
  109. data/lib/kimera/operators/range.rb +15 -0
  110. data/lib/kimera/operators/regexp_literal.rb +16 -0
  111. data/lib/kimera/operators/respond_to_guard.rb +24 -0
  112. data/lib/kimera/operators/return_value.rb +36 -0
  113. data/lib/kimera/operators/safe_navigation.rb +15 -0
  114. data/lib/kimera/operators/selector_swap.rb +24 -0
  115. data/lib/kimera/operators/statement_deletion.rb +26 -0
  116. data/lib/kimera/operators/string_literal.rb +22 -0
  117. data/lib/kimera/operators/symbol_literal.rb +26 -0
  118. data/lib/kimera/operators/vocabulary.rb +82 -0
  119. data/lib/kimera/operators.rb +141 -0
  120. data/lib/kimera/plugins.rb +3 -0
  121. data/lib/kimera/registry/builder.rb +59 -0
  122. data/lib/kimera/registry/mutation_point.rb +146 -0
  123. data/lib/kimera/registry/numbering.rb +15 -0
  124. data/lib/kimera/registry/registry.rb +120 -0
  125. data/lib/kimera/registry/source_file.rb +71 -0
  126. data/lib/kimera/registry/tally.rb +11 -0
  127. data/lib/kimera/registry/walking.rb +55 -0
  128. data/lib/kimera/report/actions.rb +38 -0
  129. data/lib/kimera/report/coloring.rb +25 -0
  130. data/lib/kimera/report/formats.rb +78 -0
  131. data/lib/kimera/report/live.rb +56 -0
  132. data/lib/kimera/report/log.rb +46 -0
  133. data/lib/kimera/report/progress.rb +55 -0
  134. data/lib/kimera/report/screen.rb +44 -0
  135. data/lib/kimera/report/sections.rb +50 -0
  136. data/lib/kimera/report/tally.rb +84 -0
  137. data/lib/kimera/report/text.rb +104 -0
  138. data/lib/kimera/results/result.rb +72 -0
  139. data/lib/kimera/results/run_report.rb +114 -0
  140. data/lib/kimera/rewrite/ast_walk.rb +20 -0
  141. data/lib/kimera/rewrite/directive.rb +61 -0
  142. data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
  143. data/lib/kimera/rewrite/directive_handlers.rb +138 -0
  144. data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
  145. data/lib/kimera/rewrite/numbered_params.rb +50 -0
  146. data/lib/kimera/runtime.rb +51 -0
  147. data/lib/kimera/scope/config.rb +90 -0
  148. data/lib/kimera/scope/file_set.rb +51 -0
  149. data/lib/kimera/scope/ignore_list.rb +55 -0
  150. data/lib/kimera/self_protection.rb +56 -0
  151. data/lib/kimera/support/duration.rb +13 -0
  152. data/lib/kimera/support/errors.rb +8 -0
  153. data/lib/kimera/support/operator_protocol.rb +10 -0
  154. data/lib/kimera/support/syntax.rb +68 -0
  155. data/lib/kimera/support/syntax_types.rb +20 -0
  156. data/lib/kimera/support/unparse.rb +15 -0
  157. data/lib/kimera/support/version.rb +5 -0
  158. data/lib/kimera/support/warnings.rb +15 -0
  159. data/lib/kimera/synthesis/file_weave.rb +78 -0
  160. data/lib/kimera/synthesis/guard_weaver.rb +77 -0
  161. data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
  162. data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
  163. data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
  164. data/lib/kimera/synthesis/overlay.rb +52 -0
  165. data/lib/kimera/synthesis/overlay_splice.rb +101 -0
  166. data/lib/kimera/synthesis/project.rb +58 -0
  167. data/lib/kimera/synthesis/source_map.rb +42 -0
  168. data/lib/kimera/version.rb +3 -0
  169. data/lib/kimera.rb +13 -0
  170. data/skills/kimera/SKILL.md +118 -0
  171. metadata +259 -0
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "../error"
5
+
6
+ module Kimera
7
+ module Config
8
+ DEFAULT_PATH = ".kimera.yml"
9
+
10
+ SCALAR_KEYS = %w[framework source_root soft_timeout hard_timeout leak_every
11
+ jobs max_survivors max_ignored max_errors coverage isolate_db
12
+ fail_on_no_coverage progress color quiet verbose log format report baseline].freeze
13
+ LIST_KEYS = %w[paths tests operators exclude exclude_tests isolate_when_covered_by require].freeze
14
+
15
+ SCOPE_KEYS = %i[paths operators exclude].freeze
16
+
17
+ module_function
18
+
19
+ def parse(argv)
20
+ index = argv.index("--config")
21
+ return root unless index
22
+ file = argv[index + 1]
23
+ raise(UsageError, "no such config file: #{file}") unless file && File.exist?(file)
24
+ load(file)
25
+ end
26
+
27
+ def prefer(from_cli, from_config, default)
28
+ cli = Array(from_cli)
29
+ chosen = cli.empty? ? Array(from_config) : cli
30
+ chosen.empty? ? default.dup : chosen
31
+ end
32
+
33
+ def document(**options) = options.transform_keys(&:to_s)
34
+
35
+ def scope(defaults, argv)
36
+ defaults.merge(parse(argv).slice(*SCOPE_KEYS))
37
+ end
38
+
39
+ def load(file)
40
+ return {} unless File.exist?(file)
41
+ options = normalize(YAML.safe_load_file(file) || {})
42
+ baseline = options[:baseline]
43
+ baseline ? options.merge(ignore: Array(options[:ignore]) + ignores_from(baseline, file)) : options
44
+ end
45
+
46
+ def root(root: ".")
47
+ load(File.join(root, DEFAULT_PATH))
48
+ end
49
+
50
+ def normalize(data)
51
+ options = scalars(data)
52
+ options[:ignore] = Array(data["ignore"]).map { |r| validate(symbolize(r)) } if data.key?("ignore")
53
+ options
54
+ end
55
+
56
+ def ignores_from(baseline, file)
57
+ anchored = File.expand_path(baseline, File.dirname(file))
58
+ raise(UsageError, "no such baseline file: #{anchored}") unless File.file?(anchored)
59
+ Array((YAML.safe_load_file(anchored) || {})["ignore"]).map { |rule| validate(symbolize(rule)) }
60
+ end
61
+
62
+ def scalars(data)
63
+ (SCALAR_KEYS + LIST_KEYS).each_with_object({}) do |key, options|
64
+ next unless data.key?(key)
65
+ value = data[key]
66
+ options[key.to_sym] = LIST_KEYS.include?(key) ? Array(value) : value
67
+ end
68
+ end
69
+
70
+ def symbolize(rule)
71
+ rule.transform_keys(&:to_sym)
72
+ end
73
+
74
+ def validate(rule)
75
+ file = rule[:file]
76
+ raise(UsageError, "ignore entry missing file: #{rule.inspect}") unless file
77
+ reason(rule, file)
78
+ rule
79
+ end
80
+
81
+ def reason(rule, file)
82
+ return unless reasonless?(rule)
83
+ raise(UsageError, "ignore entry for #{file} needs a reason: (why is this mutant equivalent?)")
84
+ end
85
+
86
+ def reasonless?(rule)
87
+ rule[:reason].to_s.strip.empty?
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config"
4
+
5
+ module Kimera
6
+ module FileSet
7
+ DEFAULT_GLOBS = ["app/**/*.rb", "lib/**/*.rb"].freeze
8
+
9
+ module_function
10
+
11
+ def globs(from_cli, options)
12
+ Config.prefer(from_cli, options[:paths], DEFAULT_GLOBS)
13
+ end
14
+
15
+ def scoped(from_cli, options)
16
+ expand(globs(from_cli, options), exclude: options[:exclude])
17
+ end
18
+
19
+ def expand(patterns, include: [], exclude: [])
20
+ exclude((Array(patterns) + Array(include)).flat_map { |pattern| resolve(pattern) }.uniq, exclude).sort
21
+ end
22
+
23
+ def relative(path, root)
24
+ absolute = File.expand_path(path)
25
+ absolute.start_with?("#{root}/") ? absolute[(root.length + 1)..] : path
26
+ end
27
+
28
+ def resolve(pattern)
29
+ return Dir.glob(File.join(pattern, "**", "*.rb")) if File.directory?(pattern)
30
+ Dir.glob(pattern).select { |f| File.file?(f) && f.end_with?(".rb") }
31
+ end
32
+
33
+ def unused(patterns, exclude)
34
+ files = expand(patterns)
35
+ Array(exclude).reject do |pattern|
36
+ resolved = resolve(pattern).to_set
37
+ files.any? { |file| resolved.include?(file) || matches?(file, [pattern]) }
38
+ end
39
+ end
40
+
41
+ def exclude(files, exclude)
42
+ patterns = Array(exclude)
43
+ excluded = patterns.flat_map { |p| resolve(p) }.to_set
44
+ files.reject { |file| excluded.include?(file) || matches?(file, patterns) }
45
+ end
46
+
47
+ def matches?(file, patterns)
48
+ Array(patterns).any? { |p| File.fnmatch?(p, file, File::FNM_PATHNAME) }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module IgnoreList
5
+ Match =
6
+ Data.define(:point, :mutant) do
7
+ def rule?(rule)
8
+ glob = rule[:file]
9
+ glob && File.fnmatch?(glob, point.file, File::FNM_PATHNAME) && anchors?(rule)
10
+ end
11
+ private
12
+ def line?(value) = point.location.range.include?(Integer(value))
13
+ def column?(value) = point.location.start_column == Integer(value)
14
+ def label?(value) = value == mutant.label
15
+ def method?(value) = value.to_s == point.method_name
16
+ def original?(value) = squeeze(value) == squeeze(point.original_source)
17
+ def squeeze(source) = source.to_s.split.join(" ")
18
+
19
+ def anchors?(rule)
20
+ ANCHORS.all? do |field, matcher|
21
+ value = rule[field]
22
+ !value || __send__(matcher, value)
23
+ end
24
+ end
25
+ end
26
+
27
+ module_function
28
+
29
+ def ids(registry, rules)
30
+ rules = Array(rules)
31
+ registry.each.filter_map { |mutant, point| mutant.id if rules.any? { |rule| match?(rule, point, mutant) } }
32
+ end
33
+
34
+ def stale(registry, rules)
35
+ Array(rules).select do |rule|
36
+ relevant?(rule, registry) && !any?(rule, registry)
37
+ end
38
+ end
39
+
40
+ def relevant?(rule, registry)
41
+ glob = rule[:file]
42
+ glob && registry.files.any? { |file| File.fnmatch?(glob, file, File::FNM_PATHNAME) }
43
+ end
44
+
45
+ def any?(rule, registry)
46
+ registry.each.any? { |mutant, point| match?(rule, point, mutant) }
47
+ end
48
+
49
+ ANCHORS = { line: :line?, column: :column?, label: :label? }.merge(method: :method?, original: :original?).freeze
50
+
51
+ def match?(rule, point, mutant)
52
+ Match.new(point, mutant).rule?(rule)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "runtime"
4
+
5
+ module Kimera
6
+ module SelfProtection
7
+ module_function
8
+
9
+ OUT_OF_PROCESS_ENTRYPOINTS = %w[
10
+ execution/isolated_child.rb
11
+ execution/isolated_child_minitest.rb
12
+ ].map { |path| File.expand_path(path, __dir__) }.freeze
13
+
14
+ def paths
15
+ [Runtime::SOURCE_PATH, *OUT_OF_PROCESS_ENTRYPOINTS]
16
+ end
17
+
18
+ HARNESS_CRITICAL_SUFFIXES = %w[
19
+ kimera/frameworks/adapter.rb
20
+ kimera/frameworks/rspec_adapter.rb
21
+ kimera/frameworks/rspec_group_index.rb
22
+ kimera/frameworks/minitest_adapter.rb
23
+ kimera/execution/shift.rb
24
+ kimera/execution/shift/attempt.rb
25
+ kimera/execution/shift/coverage_channel.rb
26
+ kimera/execution/shift/killer_memory.rb
27
+ kimera/execution/shift/leak_guard.rb
28
+ kimera/execution/harness.rb
29
+ kimera/execution/baseline_pass.rb
30
+ kimera/execution/reload.rb
31
+ kimera/execution/parallel_test_databases.rb
32
+ kimera/execution/verdicts.rb
33
+ kimera/execution/worker_pool.rb
34
+ kimera/execution/worker_pool/fleet.rb
35
+ kimera/execution/child_process.rb
36
+ kimera/execution/rig.rb
37
+ kimera/registry/builder.rb
38
+ ].freeze
39
+
40
+ def critical?(path)
41
+ text = path.to_s
42
+ HARNESS_CRITICAL_SUFFIXES.any? { |suffix| text.end_with?(suffix) }
43
+ end
44
+
45
+ def protected?(path)
46
+ target = canonical(path)
47
+ paths.any? { |own| canonical(own) == target }
48
+ end
49
+
50
+ def canonical(path)
51
+ File.realpath(path)
52
+ rescue SystemCallError
53
+ File.expand_path(path)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ Duration =
5
+ Data.define(:seconds) do
6
+ def clock = format("%d:%02d", seconds / 60, seconds % 60)
7
+
8
+ def brief
9
+ return format("%.1fs", seconds) if seconds >= 1
10
+ format(seconds >= 0.01 ? "%.0fms" : "%.1fms", seconds * 1000)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ end
5
+
6
+ class Kimera::Error < StandardError; end
7
+
8
+ class Kimera::UsageError < Kimera::Error; end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module OperatorProtocol
5
+ TYPE = "type"
6
+ INDEX = "index"
7
+ STATEMENT_DELETION = "statement_deletion"
8
+ Variant = Struct.new(:label, :directive, keyword_init: true)
9
+ end
10
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Kimera
6
+ module Syntax
7
+ %i[
8
+ AndNode ArrayNode AssocNode AssocSplatNode BlockArgumentNode BlockNode
9
+ CallNode ClassVariableOrWriteNode ClassVariableReadNode ClassVariableWriteNode
10
+ CallOperatorWriteNode ClassVariableOperatorWriteNode ConstantOperatorWriteNode
11
+ ConstantPathOperatorWriteNode GlobalVariableOperatorWriteNode
12
+ InstanceVariableOperatorWriteNode LocalVariableOperatorWriteNode
13
+ ConstantPathWriteNode ConstantWriteNode DefNode DefinedNode FalseNode FloatNode
14
+ ForwardingArgumentsNode GlobalVariableOrWriteNode GlobalVariableReadNode
15
+ GlobalVariableWriteNode HashNode IfNode InNode InstanceVariableOrWriteNode
16
+ InstanceVariableReadNode InstanceVariableWriteNode IntegerNode KeywordHashNode
17
+ LambdaNode MatchPredicateNode MatchRequiredNode NilNode Node
18
+ OptionalKeywordParameterNode OptionalParameterNode OrNode ParenthesesNode
19
+ RangeNode RegularExpressionNode ReturnNode SplatNode StatementsNode StringNode
20
+ SymbolNode TrueNode UnlessNode
21
+ ].each { |name| const_set(name, Prism.const_get(name)) }
22
+
23
+ OPERATOR_WRITES = [
24
+ CallOperatorWriteNode, ClassVariableOperatorWriteNode, ConstantOperatorWriteNode,
25
+ ConstantPathOperatorWriteNode, GlobalVariableOperatorWriteNode,
26
+ InstanceVariableOperatorWriteNode, LocalVariableOperatorWriteNode
27
+ ].freeze
28
+
29
+ View =
30
+ Data.define(:node) do
31
+ def collection(splats)
32
+ [node.elements, splats[node.class]] if splats.key?(node.class)
33
+ end
34
+
35
+ def unwrap(names)
36
+ return unless node.is_a?(CallNode) && node.receiver && names.include?(node.name)
37
+ node.name unless node.arguments || node.block
38
+ end
39
+
40
+ def operator
41
+ node.binary_operator if OPERATOR_WRITES.any? { |kind| node.is_a?(kind) }
42
+ end
43
+
44
+ def diagnostic?(names, loggers)
45
+ receiver = node.receiver
46
+ return names.include?(node.name) unless receiver
47
+ logger?(receiver, loggers)
48
+ end
49
+ private
50
+ def logger?(receiver, loggers)
51
+ while receiver
52
+ return true if named?(receiver, loggers)
53
+ receiver = receiver.is_a?(CallNode) ? receiver.receiver : nil
54
+ end
55
+ false
56
+ end
57
+
58
+ def named?(receiver, names)
59
+ receiver.class.method_defined?(:name) && names.include?(receiver.name)
60
+ end
61
+ end
62
+
63
+ module_function
64
+
65
+ def parse(source) = Prism.parse(source)
66
+ def view(node) = View.new(node)
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "syntax"
4
+
5
+ module Kimera
6
+ module SyntaxTypes
7
+ Kimera::Syntax.constants.grep(/Node\z/).each do |name|
8
+ const_set(name, Kimera::Syntax.const_get(name))
9
+ end
10
+
11
+ Installer =
12
+ Data.define(:target) do
13
+ def call
14
+ SyntaxTypes.constants.grep(/Node\z/).each do |name|
15
+ target.const_set(name, SyntaxTypes.const_get(name)) unless target.constants.include?(name)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "warnings"
4
+
5
+ Kimera::Warnings.silence { require "unparser" }
6
+
7
+ module Kimera
8
+ module Unparse
9
+ module_function
10
+
11
+ def parse(source) = Kimera::Warnings.silence { Unparser.parse(source) }
12
+
13
+ def unparse(node) = Kimera::Warnings.silence { Unparser.unparse(node) }
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Warnings
5
+ module_function
6
+
7
+ def silence
8
+ previous = $VERBOSE
9
+ $VERBOSE = nil
10
+ yield
11
+ ensure
12
+ $VERBOSE = previous
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/unparse"
4
+ require_relative "overlay_splice"
5
+
6
+ class Kimera::Overlay::FileWeave
7
+ include Kimera::OverlaySplice
8
+
9
+ def initialize(file, map, safe, unsafe)
10
+ @file = file
11
+ @map = map
12
+ @safe = safe
13
+ @unsafe = unsafe
14
+ end
15
+
16
+ def result
17
+ attempt(@safe) || fallback
18
+ end
19
+
20
+ private
21
+
22
+ def io = @_io ||= $stderr
23
+
24
+ def attempt(points)
25
+ render(Kimera::Guardrail.new(@map, points))
26
+ rescue StandardError
27
+ nil
28
+ end
29
+
30
+ def render(weaver)
31
+ Kimera::Overlay::Result.new(
32
+ file: @file, source: @map.restore(Kimera::Unparse.unparse(weaver.tree)), mutant_ids: weaver.applied,
33
+ skipped_unsafe: @unsafe.flat_map(&:ids)
34
+ )
35
+ end
36
+
37
+ def fallback
38
+ viable = bisect(@safe)
39
+ return recover if viable.empty?
40
+ finish(viable)
41
+ end
42
+
43
+ def finish(viable)
44
+ dropped = @safe - viable
45
+ notice(dropped)
46
+ result = attempt(viable)
47
+ result.skipped_unsafe.concat(dropped.flat_map(&:ids))
48
+ result
49
+ end
50
+
51
+ def notice(dropped)
52
+ io.puts(
53
+ "kimera: #{@file}: #{dropped.size} mutation point(s) at " \
54
+ "#{dropped.map { |p| "line #{p.location.start_line}" }.uniq.join(", ")} " \
55
+ "are unguardable (unparser round-trip); their mutants are reported no_coverage"
56
+ )
57
+ end
58
+
59
+ def recover
60
+ spliced = splice
61
+ return spliced if spliced
62
+ @map.restore(Kimera::Unparse.unparse(Kimera::Guardrail.new(@map, @safe).tree))
63
+ end
64
+
65
+ def bisect(points)
66
+ return points if attempt(points)
67
+ count = points.size
68
+ return [] if count <= 1
69
+ merge(points, count)
70
+ end
71
+
72
+ def merge(points, count)
73
+ half = count / 2
74
+ merged = bisect(points[0...half]) + bisect(points[half..])
75
+ merged.pop while merged.any? && !attempt(merged)
76
+ merged
77
+ end
78
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/unparse"
4
+ require_relative "../rewrite/ast_walk"
5
+ require_relative "../rewrite/directive"
6
+ require_relative "guardrail_dispatch"
7
+ require_relative "guardrail_interpolation"
8
+ require_relative "guardrail_value_objects"
9
+ require_relative "source_map"
10
+
11
+ class Kimera::Guardrail
12
+ include Kimera::GuardrailDispatch
13
+ include Kimera::GuardrailInterpolation
14
+ include Kimera::GuardrailValueObjects
15
+
16
+ attr_reader :applied
17
+
18
+ Patch =
19
+ Data.define(:map, :span, :directive) do
20
+ def apply(rebuilt, original)
21
+ expression = original.location&.expression
22
+ return rebuilt unless expression && map.span(expression) == span
23
+ Kimera::Rewrite::Directive.apply(rebuilt, directive)
24
+ end
25
+ end
26
+
27
+ def initialize(map, points)
28
+ @map = map
29
+ @points = points
30
+ @applied = []
31
+ end
32
+
33
+ def tree
34
+ transform(@map.ast)
35
+ end
36
+
37
+ def rewrite(node)
38
+ [Kimera::Unparse.unparse(transform(node)), applied]
39
+ end
40
+
41
+ def transform(node)
42
+ Kimera::Rewrite::AstWalk.rebuild(node) { |rebuilt, original| rebuild(rebuilt, original) }
43
+ end
44
+
45
+ def bake(location, directive)
46
+ prune(overlay([location.start_offset, location.finish], directive))
47
+ end
48
+
49
+ private
50
+
51
+ def rebuild(rebuilt, original)
52
+ rebuilt = flatten(rebuilt) if rebuilt.type == :dstr
53
+ rebuilt = dispatch(rebuilt, points(original))
54
+ reopen(rebuilt)
55
+ end
56
+
57
+ def overlay(target, directive)
58
+ patch = Patch.new(@map, target, directive)
59
+ Kimera::Rewrite::AstWalk.rebuild(@map.ast) do |rebuilt, original|
60
+ reopen(patch.apply(rebuilt, original))
61
+ end
62
+ end
63
+
64
+ def points(node)
65
+ expr = node.location&.expression
66
+ return [] unless expr
67
+ ranges.fetch(@map.span(expr), [])
68
+ end
69
+
70
+ def ranges
71
+ @_ranges ||= group(@points)
72
+ end
73
+
74
+ def ast(type, *children)
75
+ Parser::AST::Node.new(type, children)
76
+ end
77
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module GuardrailDispatch
5
+ private
6
+
7
+ def group(points)
8
+ points.group_by do |point|
9
+ location = point.location
10
+ [location.start_offset, location.finish]
11
+ end
12
+ end
13
+
14
+ def dispatch(original, points)
15
+ mutants = points.flat_map(&:mutants).reverse
16
+ return parameter(original, mutants) if %i[optarg kwoptarg].include?(original.type)
17
+ guarded = guard(mutants, original) { |mutant| variant(original, mutant) }
18
+ guarded.equal?(original) ? guarded : ast(:begin, guarded)
19
+ end
20
+
21
+ def branch(mutant, truthy, dispatch)
22
+ id = mutant.id
23
+ @applied << id
24
+ conditional(active(id), truthy, dispatch)
25
+ end
26
+
27
+ def variant(original, mutant)
28
+ Kimera::Rewrite::Directive.apply(original, mutant.directive, unguard: method(:unwrap))
29
+ end
30
+
31
+ def parameter(original, mutants)
32
+ name, default = original.children
33
+ original.updated(nil, [name, ast(:begin, guard(mutants, default) { missing(original, name) })])
34
+ end
35
+
36
+ def guard(mutants, seed) = mutants.reduce(seed) { |chain, mutant| branch(mutant, yield(mutant), chain) }
37
+
38
+ def missing(param, name)
39
+ ast(
40
+ :send, nil, :raise, ast(:const, nil, :ArgumentError),
41
+ ast(:str, "missing #{param.type == :kwoptarg ? "keyword" : "argument"}: #{name}")
42
+ )
43
+ end
44
+
45
+ def unwrap(node)
46
+ return node unless type?(node, :begin)
47
+ inner, extra = node.children
48
+ return node unless guarded?(inner, extra)
49
+ unwrap(inner.children[2])
50
+ end
51
+
52
+ def guarded?(inner, extra)
53
+ !extra && type?(inner, :if) && active?(inner.children[0])
54
+ end
55
+
56
+ def active?(node)
57
+ type?(node, :send) && node.children[1] == :active?
58
+ end
59
+
60
+ def active(id)
61
+ ast(:send, ast(:const, ast(:cbase), :MutantRuntime), :active?, ast(:int, id))
62
+ end
63
+
64
+ def conditional(condition, truthy, fallback)
65
+ ast(:if, condition, truthy, fallback)
66
+ end
67
+ end
68
+ end