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.
- checksums.yaml +7 -0
- data/.kimera.yml.example +59 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/exe/kimera +22 -0
- data/lib/kimera/audit/operator_audit.rb +71 -0
- data/lib/kimera/cli/argv.rb +15 -0
- data/lib/kimera/cli/baseline.rb +87 -0
- data/lib/kimera/cli/changed.rb +27 -0
- data/lib/kimera/cli/ci.rb +27 -0
- data/lib/kimera/cli/completion.rb +37 -0
- data/lib/kimera/cli/doctor.rb +115 -0
- data/lib/kimera/cli/flag.rb +55 -0
- data/lib/kimera/cli/help_text.rb +45 -0
- data/lib/kimera/cli/init.rb +105 -0
- data/lib/kimera/cli/mutant.rb +82 -0
- data/lib/kimera/cli/report_file.rb +20 -0
- data/lib/kimera/cli/run/arguments.rb +58 -0
- data/lib/kimera/cli/run/cycle.rb +78 -0
- data/lib/kimera/cli/run/digest.rb +49 -0
- data/lib/kimera/cli/run/emission.rb +45 -0
- data/lib/kimera/cli/run/gate.rb +43 -0
- data/lib/kimera/cli/run/options.rb +85 -0
- data/lib/kimera/cli/run/pass.rb +100 -0
- data/lib/kimera/cli/run/sources.rb +56 -0
- data/lib/kimera/cli/run.rb +52 -0
- data/lib/kimera/cli/skill.rb +23 -0
- data/lib/kimera/cli/suggestion.rb +36 -0
- data/lib/kimera/cli/survivors/panel.rb +81 -0
- data/lib/kimera/cli/survivors.rb +89 -0
- data/lib/kimera/cli/synthesize.rb +66 -0
- data/lib/kimera/cli/workflows.rb +8 -0
- data/lib/kimera/cli.rb +132 -0
- data/lib/kimera/error.rb +3 -0
- data/lib/kimera/execution/baseline_failure.rb +47 -0
- data/lib/kimera/execution/baseline_pass.rb +123 -0
- data/lib/kimera/execution/boot.rb +50 -0
- data/lib/kimera/execution/callback.rb +33 -0
- data/lib/kimera/execution/child_process.rb +48 -0
- data/lib/kimera/execution/composite_isolation.rb +24 -0
- data/lib/kimera/execution/harness.rb +150 -0
- data/lib/kimera/execution/isolated.rb +64 -0
- data/lib/kimera/execution/isolated_child.rb +15 -0
- data/lib/kimera/execution/isolated_child_command.rb +44 -0
- data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
- data/lib/kimera/execution/isolated_plan.rb +54 -0
- data/lib/kimera/execution/isolated_scheduling.rb +54 -0
- data/lib/kimera/execution/isolated_test_selection.rb +22 -0
- data/lib/kimera/execution/isolated_verdict.rb +72 -0
- data/lib/kimera/execution/isolated_watchdog.rb +59 -0
- data/lib/kimera/execution/isolation.rb +17 -0
- data/lib/kimera/execution/null_progress.rb +17 -0
- data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
- data/lib/kimera/execution/overlay_guards.rb +56 -0
- data/lib/kimera/execution/parallel_test_databases.rb +64 -0
- data/lib/kimera/execution/pool_driver.rb +101 -0
- data/lib/kimera/execution/priority.rb +23 -0
- data/lib/kimera/execution/reload.rb +104 -0
- data/lib/kimera/execution/rig.rb +41 -0
- data/lib/kimera/execution/schedule.rb +105 -0
- data/lib/kimera/execution/schemata.rb +88 -0
- data/lib/kimera/execution/shift/attempt.rb +34 -0
- data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
- data/lib/kimera/execution/shift/killer_memory.rb +20 -0
- data/lib/kimera/execution/shift/leak_guard.rb +34 -0
- data/lib/kimera/execution/shift.rb +137 -0
- data/lib/kimera/execution/sweep.rb +30 -0
- data/lib/kimera/execution/transaction_isolation.rb +32 -0
- data/lib/kimera/execution/trial.rb +16 -0
- data/lib/kimera/execution/verdicts.rb +78 -0
- data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
- data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
- data/lib/kimera/execution/worker_pool/worker.rb +32 -0
- data/lib/kimera/execution/worker_pool.rb +101 -0
- data/lib/kimera/frameworks/adapter.rb +82 -0
- data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
- data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
- data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
- data/lib/kimera/incremental/git_diff.rb +98 -0
- data/lib/kimera/incremental/selection.rb +27 -0
- data/lib/kimera/incremental/session.rb +109 -0
- data/lib/kimera/memoization.rb +104 -0
- data/lib/kimera/operators/argument_drop.rb +19 -0
- data/lib/kimera/operators/arithmetic.rb +12 -0
- data/lib/kimera/operators/audit.rb +3 -0
- data/lib/kimera/operators/base.rb +40 -0
- data/lib/kimera/operators/binary_swap.rb +23 -0
- data/lib/kimera/operators/boolean.rb +26 -0
- data/lib/kimera/operators/chain_link_deletion.rb +25 -0
- data/lib/kimera/operators/collection_literal.rb +18 -0
- data/lib/kimera/operators/comparison.rb +13 -0
- data/lib/kimera/operators/conditional.rb +36 -0
- data/lib/kimera/operators/default_argument.rb +17 -0
- data/lib/kimera/operators/element_drop.rb +31 -0
- data/lib/kimera/operators/index_fetch.rb +15 -0
- data/lib/kimera/operators/kernel_coercion.rb +17 -0
- data/lib/kimera/operators/method_unwrap.rb +20 -0
- data/lib/kimera/operators/negation.rb +15 -0
- data/lib/kimera/operators/node_swap.rb +20 -0
- data/lib/kimera/operators/numeric_literal.rb +39 -0
- data/lib/kimera/operators/op_assign.rb +18 -0
- data/lib/kimera/operators/plugins.rb +27 -0
- data/lib/kimera/operators/rails_association.rb +24 -0
- data/lib/kimera/operators/rails_callback.rb +49 -0
- data/lib/kimera/operators/rails_declaration.rb +16 -0
- data/lib/kimera/operators/rails_permit.rb +34 -0
- data/lib/kimera/operators/rails_validation.rb +17 -0
- data/lib/kimera/operators/range.rb +15 -0
- data/lib/kimera/operators/regexp_literal.rb +16 -0
- data/lib/kimera/operators/respond_to_guard.rb +24 -0
- data/lib/kimera/operators/return_value.rb +36 -0
- data/lib/kimera/operators/safe_navigation.rb +15 -0
- data/lib/kimera/operators/selector_swap.rb +24 -0
- data/lib/kimera/operators/statement_deletion.rb +26 -0
- data/lib/kimera/operators/string_literal.rb +22 -0
- data/lib/kimera/operators/symbol_literal.rb +26 -0
- data/lib/kimera/operators/vocabulary.rb +82 -0
- data/lib/kimera/operators.rb +141 -0
- data/lib/kimera/plugins.rb +3 -0
- data/lib/kimera/registry/builder.rb +59 -0
- data/lib/kimera/registry/mutation_point.rb +146 -0
- data/lib/kimera/registry/numbering.rb +15 -0
- data/lib/kimera/registry/registry.rb +120 -0
- data/lib/kimera/registry/source_file.rb +71 -0
- data/lib/kimera/registry/tally.rb +11 -0
- data/lib/kimera/registry/walking.rb +55 -0
- data/lib/kimera/report/actions.rb +38 -0
- data/lib/kimera/report/coloring.rb +25 -0
- data/lib/kimera/report/formats.rb +78 -0
- data/lib/kimera/report/live.rb +56 -0
- data/lib/kimera/report/log.rb +46 -0
- data/lib/kimera/report/progress.rb +55 -0
- data/lib/kimera/report/screen.rb +44 -0
- data/lib/kimera/report/sections.rb +50 -0
- data/lib/kimera/report/tally.rb +84 -0
- data/lib/kimera/report/text.rb +104 -0
- data/lib/kimera/results/result.rb +72 -0
- data/lib/kimera/results/run_report.rb +114 -0
- data/lib/kimera/rewrite/ast_walk.rb +20 -0
- data/lib/kimera/rewrite/directive.rb +61 -0
- data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
- data/lib/kimera/rewrite/directive_handlers.rb +138 -0
- data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
- data/lib/kimera/rewrite/numbered_params.rb +50 -0
- data/lib/kimera/runtime.rb +51 -0
- data/lib/kimera/scope/config.rb +90 -0
- data/lib/kimera/scope/file_set.rb +51 -0
- data/lib/kimera/scope/ignore_list.rb +55 -0
- data/lib/kimera/self_protection.rb +56 -0
- data/lib/kimera/support/duration.rb +13 -0
- data/lib/kimera/support/errors.rb +8 -0
- data/lib/kimera/support/operator_protocol.rb +10 -0
- data/lib/kimera/support/syntax.rb +68 -0
- data/lib/kimera/support/syntax_types.rb +20 -0
- data/lib/kimera/support/unparse.rb +15 -0
- data/lib/kimera/support/version.rb +5 -0
- data/lib/kimera/support/warnings.rb +15 -0
- data/lib/kimera/synthesis/file_weave.rb +78 -0
- data/lib/kimera/synthesis/guard_weaver.rb +77 -0
- data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
- data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
- data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
- data/lib/kimera/synthesis/overlay.rb +52 -0
- data/lib/kimera/synthesis/overlay_splice.rb +101 -0
- data/lib/kimera/synthesis/project.rb +58 -0
- data/lib/kimera/synthesis/source_map.rb +42 -0
- data/lib/kimera/version.rb +3 -0
- data/lib/kimera.rb +13 -0
- data/skills/kimera/SKILL.md +118 -0
- 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,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
|