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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Kimera::Operators::SafeNavigation < Kimera::Operators::Base
6
+ class << self
7
+ def key = "safe_navigation"
8
+ end
9
+
10
+ def variants(node, **)
11
+ return unless node.is_a?(CallNode)
12
+ return unless node.safe_navigation?
13
+ solo("&. => .", "csend_to_send")
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Kimera::Operators::SelectorSwap < Kimera::Operators::Base
6
+ SWAPS = { select: :reject, reject: :select, filter_map: :map }
7
+ .merge({ all?: :any?, any?: :all? })
8
+ .merge({ min: :max, max: :min })
9
+ .merge({ first: :last, last: :first })
10
+ .merge({ detect: :first, find: :first })
11
+ .freeze
12
+
13
+ class << self
14
+ def key = "selector_swap"
15
+ end
16
+
17
+ def variants(node, **)
18
+ return unless node.is_a?(CallNode) && node.receiver
19
+ name = node.name
20
+ to = SWAPS[name]
21
+ return unless to
22
+ solo("#{name} => #{to}", "selector_swap", to: to.to_s)
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Kimera::Operators::StatementDeletion < Kimera::Operators::Base
6
+ DIAGNOSTIC_NAMES = %i[puts print pp p].freeze
7
+ LOGGER = %i[logger @logger].freeze
8
+
9
+ class << self
10
+ def key = Kimera::Operators::STATEMENT_DELETION
11
+
12
+ def statement? = true
13
+ end
14
+
15
+ def variants(node, **)
16
+ return unless identifier?(node)
17
+ return if diagnostic?(node)
18
+ deletion(node)
19
+ end
20
+
21
+ private
22
+
23
+ def diagnostic?(node)
24
+ syntax(node).diagnostic?(DIAGNOSTIC_NAMES, LOGGER)
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Kimera::Operators::StringLiteral < Kimera::Operators::Base
6
+ class << self
7
+ def key = "string_literal"
8
+ end
9
+
10
+ def variants(node, **)
11
+ return unless node.is_a?(StringNode)
12
+ value = node.unescaped
13
+ return if value.empty?
14
+ solo("#{excerpt(value)} => \"\"", "string_literal", value: "")
15
+ end
16
+
17
+ private
18
+
19
+ def excerpt(value)
20
+ value.inspect[0, 30]
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Kimera::Operators::SymbolLiteral < Kimera::Operators::Base
6
+ SUFFIX = "__kimera__"
7
+
8
+ class << self
9
+ def key = "symbol_literal"
10
+ end
11
+
12
+ def variants(node, **)
13
+ return unless node.is_a?(SymbolNode)
14
+ return if node.location.slice.end_with?(":")
15
+ name = node.unescaped
16
+ return if name.empty?
17
+ tagged(name)
18
+ end
19
+
20
+ private
21
+
22
+ def tagged(name)
23
+ stem = name[0, 20]
24
+ solo(":#{stem} => :#{stem}#{SUFFIX}", "symbol_literal", value: "#{name}#{SUFFIX}")
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/syntax"
4
+
5
+ module Kimera
6
+ module Operators
7
+ Kimera::SyntaxTypes::Installer.new(self).call
8
+ TYPE = "type"
9
+ INDEX = "index"
10
+ STATEMENT_DELETION = "statement_deletion"
11
+
12
+ IDENTIFIER = /\A[A-Za-z_]\w*[!?]?\z/
13
+
14
+ Variant = Struct.new(:label, :directive, keyword_init: true)
15
+
16
+ CallQuery =
17
+ Data.define(:node) do
18
+ def named?(names)
19
+ name = node.name
20
+ names.is_a?(Symbol) ? name == names : names.include?(name)
21
+ end
22
+
23
+ def call?(names) = node.is_a?(SyntaxTypes::CallNode) && named?(names)
24
+ def chained?(names) = call?(names) && !!node.receiver
25
+ def bare?(names) = call?(names) && !node.receiver
26
+ end
27
+
28
+ module Vocabulary
29
+ module_function
30
+
31
+ def directive(type, **fields)
32
+ { TYPE => type }.merge(fields.transform_keys(&:to_s))
33
+ end
34
+
35
+ def solo(label, type, **fields)
36
+ [Variant.new(label: label, directive: directive(type, **fields))]
37
+ end
38
+
39
+ def deletion(node)
40
+ solo("delete `#{first(node)}`", STATEMENT_DELETION)
41
+ end
42
+
43
+ def drops(nodes, type, label:)
44
+ nodes.each_index.map do |index|
45
+ Variant.new(label: "#{label} `#{first(nodes[index])}`", directive: directive(type, index: index))
46
+ end
47
+ end
48
+
49
+ def call(node) = CallQuery.new(node)
50
+
51
+ { matches?: :call?, chained?: :chained?, bare?: :bare? }.each do |name, query|
52
+ define_method(name) { |node, names| call(node).public_send(query, names) }
53
+ end
54
+
55
+ def identifier?(node)
56
+ node.is_a?(SyntaxTypes::CallNode) && node.name.to_s.match?(IDENTIFIER)
57
+ end
58
+
59
+ def arguments(node)
60
+ node.arguments&.arguments || []
61
+ end
62
+
63
+ def unary?(node)
64
+ return false if node.block
65
+ argument, extra = arguments(node)
66
+ !extra && argument.is_a?(SyntaxTypes::Node) && !argument.is_a?(SyntaxTypes::SplatNode)
67
+ end
68
+
69
+ def keywords(node)
70
+ arguments(node).reverse_each.find { |a| a.is_a?(SyntaxTypes::KeywordHashNode) }
71
+ end
72
+
73
+ def unescaped(node)
74
+ node.unescaped if node.is_a?(SyntaxTypes::SymbolNode)
75
+ end
76
+
77
+ def first(node)
78
+ node.location.slice.lines.first.to_s.strip
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "operators/argument_drop"
5
+ require_relative "operators/arithmetic"
6
+ require_relative "operators/base"
7
+ require_relative "operators/boolean"
8
+ require_relative "operators/chain_link_deletion"
9
+ require_relative "operators/collection_literal"
10
+ require_relative "operators/comparison"
11
+ require_relative "operators/conditional"
12
+ require_relative "operators/default_argument"
13
+ require_relative "operators/element_drop"
14
+ require_relative "operators/index_fetch"
15
+ require_relative "operators/kernel_coercion"
16
+ require_relative "operators/method_unwrap"
17
+ require_relative "operators/negation"
18
+ require_relative "operators/numeric_literal"
19
+ require_relative "operators/op_assign"
20
+ require_relative "operators/rails_association"
21
+ require_relative "operators/rails_callback"
22
+ require_relative "operators/rails_permit"
23
+ require_relative "operators/rails_validation"
24
+ require_relative "operators/range"
25
+ require_relative "operators/regexp_literal"
26
+ require_relative "operators/respond_to_guard"
27
+ require_relative "operators/return_value"
28
+ require_relative "operators/safe_navigation"
29
+ require_relative "operators/selector_swap"
30
+ require_relative "operators/statement_deletion"
31
+ require_relative "operators/string_literal"
32
+ require_relative "operators/symbol_literal"
33
+
34
+ module Kimera
35
+ module Operators
36
+ ALL = [
37
+ Comparison,
38
+ BooleanConnective,
39
+ BooleanLiteral,
40
+ StatementDeletion,
41
+ Arithmetic,
42
+ Negation,
43
+ SafeNavigation,
44
+ Conditional,
45
+ NumericLiteral,
46
+ StringLiteral,
47
+ CollectionLiteral,
48
+ Range,
49
+ MethodUnwrap,
50
+ ReturnValue,
51
+ SelectorSwap,
52
+ RegexpLiteral,
53
+ RailsPermit,
54
+ RailsValidation,
55
+ RailsCallback,
56
+ RailsAssociation,
57
+ ElementDrop,
58
+ ArgumentDrop,
59
+ OpAssign,
60
+ IndexFetch,
61
+ KernelCoercion,
62
+ RespondToGuard,
63
+ SymbolLiteral,
64
+ DefaultArgument,
65
+ ChainLinkDeletion
66
+ ].freeze
67
+
68
+ DEFAULT_KEYS = [
69
+ Comparison, BooleanConnective, BooleanLiteral,
70
+ StatementDeletion, Negation, Conditional
71
+ ].map(&:key).freeze
72
+
73
+ RAILS_KEYS = [RailsPermit, RailsValidation, RailsCallback, RailsAssociation].map(&:key).freeze
74
+
75
+ @registered = ALL.dup
76
+
77
+ module_function
78
+
79
+ def build(keys: DEFAULT_KEYS)
80
+ requested = expand(keys)
81
+ validate!(requested)
82
+ registered.select { |operator| requested.include?(operator.key) }.map(&:new)
83
+ end
84
+
85
+ def keys
86
+ registered.map(&:key)
87
+ end
88
+
89
+ def registered
90
+ @registered
91
+ end
92
+
93
+ def register(*klasses)
94
+ klasses.flatten.each { |klass| admit(klass) }
95
+ self
96
+ end
97
+
98
+ def reset!
99
+ @registered = ALL.dup
100
+ self
101
+ end
102
+
103
+ def admit(klass)
104
+ raise(UsageError, "not an operator class: #{klass.inspect}") unless operator?(klass)
105
+ key = klass.key
106
+ taken = registered.find { |operator| operator.key == key }
107
+ raise(UsageError, "operator key already registered: #{key} (#{taken})") if taken
108
+ registered << klass
109
+ end
110
+
111
+ def operator?(klass)
112
+ klass.is_a?(Class) && klass < Base
113
+ end
114
+
115
+ def custom
116
+ registered - ALL
117
+ end
118
+
119
+ def expand(keys)
120
+ Array(keys).map(&:to_s).flat_map { |key| expansion(key) }.uniq
121
+ end
122
+
123
+ def groups
124
+ { "all" => keys, "rails" => RAILS_KEYS, "custom" => custom.map(&:key) }
125
+ end
126
+
127
+ def expansion(key)
128
+ groups.fetch(key) { [key] }
129
+ end
130
+
131
+ def vocabulary
132
+ ["all", "rails", "custom", *keys].join(", ")
133
+ end
134
+
135
+ def validate!(requested)
136
+ unknown = requested - keys
137
+ return if unknown.empty?
138
+ raise(UsageError, "unknown operator(s): #{unknown.join(", ")} (valid: #{vocabulary})")
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "operators/plugins"
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operators"
4
+ require_relative "../self_protection"
5
+ require_relative "mutation_point"
6
+ require_relative "registry"
7
+
8
+ class Kimera::RegistryScan
9
+ def initialize(operators: nil, root: nil, shielded: true)
10
+ @chosen = operators
11
+ @base = root
12
+ @shielded = shielded
13
+ end
14
+
15
+ def build(paths)
16
+ fresh.tap do |registry|
17
+ paths.sort.each { |path| ingest(registry, path) }
18
+ end
19
+ end
20
+
21
+ def source(source, file:)
22
+ fresh.tap { |registry| collect(registry, source, file) }
23
+ end
24
+
25
+ private
26
+
27
+ def operators = @_operators ||= @chosen || Kimera::Operators.build
28
+ def root = @_root ||= File.expand_path(@base || Dir.pwd)
29
+ def numbering = @_numbering ||= Kimera::RegistryScan::Numbering.new
30
+
31
+ def fresh
32
+ Kimera::Registry.new(operators: operators.map(&:key), root: relative(root))
33
+ end
34
+
35
+ def ingest(registry, path)
36
+ return if @shielded && Kimera::SelfProtection.protected?(path)
37
+ collect(registry, File.read(path, encoding: Encoding::UTF_8), relative(path))
38
+ rescue Errno::ENOENT
39
+ nil
40
+ end
41
+
42
+ def collect(registry, source, file)
43
+ Kimera::RegistryScan::SourceFile
44
+ .new(source, file: file, operators: operators, numbering: numbering)
45
+ .points.each { |point| registry << point }
46
+ registry
47
+ end
48
+
49
+ def relative(path)
50
+ abs = File.expand_path(path)
51
+ return "." if abs == root
52
+ return path unless abs.start_with?("#{root}/")
53
+ abs[(root.length + 1)..]
54
+ end
55
+ end
56
+
57
+ require_relative "numbering"
58
+ require_relative "source_file"
59
+ require_relative "tally"
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ Location =
5
+ Struct.new(
6
+ :start_offset, :span,
7
+ :start_line, :start_column,
8
+ :end_line, :end_column,
9
+ keyword_init: true
10
+ ) do
11
+ def finish
12
+ start_offset + span
13
+ end
14
+
15
+ def range
16
+ (start_line..end_line)
17
+ end
18
+
19
+ def within?(from, to)
20
+ interior = ((from + 1)...to)
21
+ interior.cover?(start_offset) && interior.cover?(finish)
22
+ end
23
+
24
+ def to_h
25
+ coordinates.merge(columns).merge(offsets)
26
+ end
27
+
28
+ def coordinates
29
+ { "start_line" => start_line, "end_line" => end_line }
30
+ end
31
+
32
+ def columns
33
+ { "start_column" => start_column, "end_column" => end_column }
34
+ end
35
+
36
+ def offsets
37
+ { "start_offset" => start_offset, "length" => span }
38
+ end
39
+ class << self
40
+ def parse(source)
41
+ new(**extent(source), **corners(source))
42
+ end
43
+
44
+ def from_h(hash) = new(**hash.transform_keys { |key| key == "length" ? :span : key.to_sym })
45
+ def extent(source) = { start_offset: source.start_offset, span: source.length }
46
+ def anchor(source) = { start_line: source.start_line, start_column: source.start_column }
47
+ def corners(source) = anchor(source).merge(end_line: source.end_line, end_column: source.end_column)
48
+ end
49
+ end
50
+
51
+ Mutant =
52
+ Struct.new(:id, :label, :directive, keyword_init: true) do
53
+ def to_h
54
+ { "id" => id, "label" => label, "directive" => directive }
55
+ end
56
+
57
+ def self.from_h(hash)
58
+ new(id: hash["id"], label: hash["label"], directive: hash["directive"])
59
+ end
60
+
61
+ def self.of(id, variant)
62
+ new(id: id, label: variant.label, directive: variant.directive)
63
+ end
64
+ end
65
+ end
66
+
67
+ class Kimera::MutationPoint
68
+ CLASS_BODY_REASON = "class-body DSL (runs at load; evaluate with --isolated)"
69
+ SAFE = true
70
+
71
+ FIELDS = %i[point_id file operator node_type location original_source method_name mutants unsafe_reason].freeze
72
+ FIELDS.each { |field| define_method(field) { @attributes[field] } }
73
+
74
+ class << self
75
+ def from_h(hash)
76
+ new(**plain(hash), **rich(hash))
77
+ end
78
+
79
+ private
80
+
81
+ def plain(hash)
82
+ { point_id: hash["point_id"], file: hash["file"], operator: hash["operator"] }
83
+ .merge(node_type: hash["node_type"], original_source: hash["original_source"])
84
+ end
85
+
86
+ def rich(hash)
87
+ { location: Kimera::Location.from_h(hash["location"]), method_name: hash["method_name"] }
88
+ .merge(unsafe_reason: hash["unsafe_reason"], schema_safe: hash.fetch("schema_safe", SAFE))
89
+ .merge(mutants: Array(hash["mutants"]).map { |m| Kimera::Mutant.from_h(m) })
90
+ end
91
+ end
92
+
93
+ def initialize(**attributes)
94
+ @attributes = attributes
95
+ end
96
+
97
+ def body?
98
+ unsafe_reason == CLASS_BODY_REASON
99
+ end
100
+
101
+ def safe
102
+ @attributes.fetch(:schema_safe, SAFE)
103
+ end
104
+
105
+ def unsafe!(reason)
106
+ @attributes[:schema_safe] = false
107
+ @attributes[:unsafe_reason] = reason
108
+ end
109
+
110
+ def taint!(ranges)
111
+ return self unless safe?
112
+ hit = ranges.find { |range| range.contains?(location.start_offset, location.finish) }
113
+ unsafe!(hit.reason) if hit
114
+ self
115
+ end
116
+
117
+ def ids
118
+ mutants.map(&:id)
119
+ end
120
+
121
+ def project(*fields)
122
+ fields.map { |field| public_send(field) }
123
+ end
124
+
125
+ def safe?
126
+ safe
127
+ end
128
+
129
+ def to_h
130
+ identity.merge(anchor).merge(verdict).merge("mutants" => mutants.map(&:to_h))
131
+ end
132
+
133
+ def identity
134
+ { "point_id" => point_id, "file" => file, "operator" => operator }
135
+ end
136
+
137
+ def anchor
138
+ { "node_type" => node_type, "location" => location.to_h, "original_source" => original_source }
139
+ end
140
+
141
+ def verdict
142
+ { "method_name" => method_name, "schema_safe" => safe, "unsafe_reason" => unsafe_reason }
143
+ end
144
+
145
+ NONE = new(mutants: []).freeze
146
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tally"
4
+
5
+ class Kimera::RegistryScan::Numbering
6
+ def point = points.next
7
+
8
+ def mutant = mutants.next
9
+
10
+ private
11
+
12
+ def points = @_points ||= Kimera::RegistryScan::Tally.new
13
+
14
+ def mutants = @_mutants ||= Kimera::RegistryScan::Tally.new
15
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "mutation_point"
5
+
6
+ class Kimera::Registry
7
+ FORMAT_VERSION = 1
8
+
9
+ attr_reader :points, :operators, :root
10
+
11
+ def initialize(points: [], operators: [], root: ".")
12
+ @points = points
13
+ @operators = operators
14
+ @root = root
15
+ end
16
+
17
+ def <<(point)
18
+ @points << point
19
+ @_groups = nil
20
+ self
21
+ end
22
+
23
+ def size
24
+ @points.size
25
+ end
26
+
27
+ def count
28
+ @points.sum { |p| p.mutants.size }
29
+ end
30
+
31
+ def files
32
+ groups.keys
33
+ end
34
+
35
+ def summary
36
+ "Found #{count} mutants at #{size} mutation " \
37
+ "points across #{files.size} files."
38
+ end
39
+
40
+ def at(file)
41
+ groups.fetch(file, [])
42
+ end
43
+
44
+ def index
45
+ @_index ||= by_mutant_id { |_mutant, point| point }
46
+ end
47
+
48
+ def mutant(id)
49
+ lookup[id]
50
+ end
51
+
52
+ def point(id)
53
+ paired[id]
54
+ end
55
+
56
+ def each
57
+ return enum_for(:each) unless block_given?
58
+ @points.each do |point|
59
+ point.mutants.each { |m| yield(m, point) }
60
+ end
61
+ end
62
+
63
+ def to_h
64
+ identity.merge(stats)
65
+ end
66
+
67
+ def identity
68
+ { "format_version" => FORMAT_VERSION, "root" => root, "operators" => operators }
69
+ end
70
+
71
+ def stats
72
+ {
73
+ "stats" => { "files" => files.size, "points" => size, "mutants" => count },
74
+ "points" => points.map(&:to_h)
75
+ }
76
+ end
77
+
78
+ def to_json(*)
79
+ JSON.generate(to_h, *)
80
+ end
81
+
82
+ def write(path)
83
+ File.write(path, JSON.pretty_generate(to_h))
84
+ path
85
+ end
86
+
87
+ class << self
88
+ def from_h(hash)
89
+ new(root: hash["root"] || ".", operators: hash["operators"] || [], points: cast(hash["points"]))
90
+ end
91
+
92
+ def load(path)
93
+ from_h(JSON.parse(File.read(path, encoding: Encoding::UTF_8)))
94
+ end
95
+
96
+ private
97
+
98
+ def cast(points)
99
+ Array(points).map { |p| Kimera::MutationPoint.from_h(p) }
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def groups
106
+ @_groups ||= @points.group_by(&:file)
107
+ end
108
+
109
+ def paired
110
+ @_paired ||= by_mutant_id { |mutant, point| [mutant, point] }
111
+ end
112
+
113
+ def lookup
114
+ @_lookup ||= by_mutant_id { |mutant, _point| mutant }
115
+ end
116
+
117
+ def by_mutant_id
118
+ each.with_object({}) { |(mutant, point), hash| hash[mutant.id] = yield(mutant, point) }
119
+ end
120
+ end