mutant 0.11.9 → 0.11.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant/ast/pattern/lexer.rb +171 -0
  3. data/lib/mutant/ast/pattern/parser.rb +194 -0
  4. data/lib/mutant/ast/pattern/source.rb +39 -0
  5. data/lib/mutant/ast/pattern/token.rb +15 -0
  6. data/lib/mutant/ast/pattern.rb +125 -0
  7. data/lib/mutant/ast/structure.rb +890 -0
  8. data/lib/mutant/bootstrap.rb +8 -7
  9. data/lib/mutant/cli/command/environment.rb +5 -3
  10. data/lib/mutant/cli/command/util.rb +17 -2
  11. data/lib/mutant/config.rb +76 -40
  12. data/lib/mutant/env.rb +1 -1
  13. data/lib/mutant/license/subscription/opensource.rb +1 -1
  14. data/lib/mutant/matcher/method.rb +8 -1
  15. data/lib/mutant/meta/example.rb +4 -1
  16. data/lib/mutant/mutation/config.rb +36 -0
  17. data/lib/mutant/mutator/node/arguments.rb +1 -1
  18. data/lib/mutant/mutator/node/begin.rb +2 -1
  19. data/lib/mutant/mutator/node/define.rb +5 -2
  20. data/lib/mutant/mutator/node/kwargs.rb +2 -2
  21. data/lib/mutant/mutator/node/literal/regex.rb +1 -1
  22. data/lib/mutant/mutator/node/literal/symbol.rb +2 -2
  23. data/lib/mutant/mutator/node/named_value/constant_assignment.rb +1 -1
  24. data/lib/mutant/mutator/node/named_value/variable_assignment.rb +2 -2
  25. data/lib/mutant/mutator/node/regexp/capture_group.rb +3 -5
  26. data/lib/mutant/mutator/node/regexp/named_group.rb +5 -5
  27. data/lib/mutant/mutator/node/resbody.rb +0 -10
  28. data/lib/mutant/mutator/node.rb +47 -1
  29. data/lib/mutant/mutator/util/array.rb +0 -17
  30. data/lib/mutant/mutator.rb +2 -26
  31. data/lib/mutant/parallel/driver.rb +18 -2
  32. data/lib/mutant/reporter/cli/printer/config.rb +1 -1
  33. data/lib/mutant/runner.rb +4 -0
  34. data/lib/mutant/subject/config.rb +5 -4
  35. data/lib/mutant/subject.rb +4 -1
  36. data/lib/mutant/transform.rb +14 -0
  37. data/lib/mutant/version.rb +1 -1
  38. data/lib/mutant/world.rb +1 -0
  39. data/lib/mutant.rb +9 -1
  40. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cfe7ef3344fe2802cd0004827386547ff6e67adadfd7117bb51d3dadb8ed53d
4
- data.tar.gz: f8aae5c34fa4ec0c98245641ab1773a8fe0917c56c045c0859016febdaa9e4e3
3
+ metadata.gz: 3c75928a9c55d93ebfad196c028271b8c82d7d9eb94e48873ac75c9ee4bb212f
4
+ data.tar.gz: 27149ec498db15629bb18c55a5840e3ea8c369542a85bf9d3658cadc78fc8f4a
5
5
  SHA512:
6
- metadata.gz: a5c05d42faaeec2ef7e70defece4cbf35cfb5d726aefcbcc97b0ffce260851a20d2ce1815bcf0776b14d7eb1e5aef94fde299c220997f3a0d20e4acfb71b18eb
7
- data.tar.gz: 2c102205b6d798214da493c263e21f587ad86354a4ff8a389ac091ec2bc212c63d563e8e5aec867c21d277e3a7881e1f3981df625324e3801eb5ff23fcf7dc20
6
+ metadata.gz: 44867690e5c1c367df8b22ec3fa5b35941328e12fd10a26a252baa3815c955fbd37f1adaaaadfafbcdc1f72280ce013d37996d001c163930c084b95b07905362
7
+ data.tar.gz: b22e71a0ae7f1ecb96345e7996e61c4ee843c7b82442f21dea3516be132503fa9c0193443f1d7da9196fbb27f23f3ef858ee58f81c03b7cda8d4f4dcced1a9a7
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module AST
5
+ class Pattern
6
+ # rubocop:disable Metrics/ClassLength
7
+ class Lexer
8
+ WHITESPACE = [' ', "\t", "\n"].to_set.freeze
9
+ STRING_PATTERN = /\A[a-zA-Z][_a-zA-Z0-9]*\z/.freeze
10
+
11
+ SINGLE_CHAR =
12
+ {
13
+ '(' => :group_start,
14
+ ')' => :group_end,
15
+ ',' => :delimiter,
16
+ '=' => :eq,
17
+ '{' => :properties_start,
18
+ '}' => :properties_end
19
+ }.freeze
20
+
21
+ def self.call(string)
22
+ new(string).__send__(:run)
23
+ end
24
+
25
+ class Error
26
+ include Anima.new(:token)
27
+
28
+ class InvalidToken < self
29
+ def display_message
30
+ <<~MESSAGE.strip
31
+ Invalid #{token.type} token:
32
+ #{token.display_location}
33
+ MESSAGE
34
+ end
35
+ end # Token
36
+ end # Error
37
+
38
+ private_class_method :new
39
+
40
+ private
41
+
42
+ def initialize(string)
43
+ @line_index = 0
44
+ @line_start = 0
45
+ @next_position = 0
46
+ @source = Source.new(string: string)
47
+ @string = string
48
+ @tokens = []
49
+ end
50
+
51
+ def run
52
+ consume
53
+
54
+ if instance_variable_defined?(:@error)
55
+ Either::Left.new(@error)
56
+ else
57
+ Either::Right.new(@tokens)
58
+ end
59
+ end
60
+
61
+ def consume
62
+ while next? && !instance_variable_defined?(:@error)
63
+ skip_whitespace
64
+
65
+ consume_char || consume_string
66
+
67
+ skip_whitespace
68
+ end
69
+ end
70
+
71
+ def consume_char
72
+ start_position = @next_position
73
+
74
+ char = peek
75
+
76
+ type = SINGLE_CHAR.fetch(char) { return }
77
+
78
+ advance_position
79
+
80
+ @tokens << token(type: type, start_position: start_position)
81
+ end
82
+
83
+ def token(type:, start_position:, value: nil)
84
+ Token.new(
85
+ type: type,
86
+ value: value,
87
+ location: Source::Location.new(
88
+ source: @source,
89
+ line_index: @line_index,
90
+ line_start: @line_start,
91
+ range: range_from(start_position)
92
+ )
93
+ )
94
+ end
95
+
96
+ def consume_string
97
+ start_position = @next_position
98
+
99
+ token = build_string(start_position, read_string_body)
100
+
101
+ if valid_string?(token.value)
102
+ @tokens << token
103
+ else
104
+ @error = Error::InvalidToken.new(token: token)
105
+ end
106
+ end
107
+
108
+ def read_string_body
109
+ string = +''
110
+
111
+ while next?
112
+ char = peek
113
+ break if SINGLE_CHAR.key?(char) || whitespace?(char)
114
+
115
+ string << char
116
+ advance_position
117
+ end
118
+
119
+ string
120
+ end
121
+
122
+ def build_string(start_position, string)
123
+ token(
124
+ type: :string,
125
+ value: string,
126
+ start_position: start_position
127
+ )
128
+ end
129
+
130
+ def range_from(start_position)
131
+ start_position...@next_position
132
+ end
133
+
134
+ def valid_string?(string)
135
+ STRING_PATTERN.match?(string)
136
+ end
137
+
138
+ def advance_position
139
+ @next_position += 1
140
+ end
141
+
142
+ def skip_whitespace
143
+ loop do
144
+ char = peek
145
+
146
+ break unless whitespace?(char)
147
+
148
+ if char.eql?("\n")
149
+ @line_start = @next_position.succ
150
+ @line_index += 1
151
+ end
152
+
153
+ advance_position
154
+ end
155
+ end
156
+
157
+ def peek
158
+ @string[@next_position]
159
+ end
160
+
161
+ def whitespace?(char)
162
+ WHITESPACE.include?(char)
163
+ end
164
+
165
+ def next?
166
+ @next_position < @string.length
167
+ end
168
+ end # Lexer
169
+ end # Pattern
170
+ end # AST
171
+ end # Mutant
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module AST
5
+ # rubocop:disable Metrics/ClassLength
6
+ # rubocop:disable Metrics/MethodLength
7
+ class Pattern
8
+ class Parser
9
+ def initialize(tokens)
10
+ @tokens = tokens
11
+ @next_position = 0
12
+ end
13
+
14
+ def self.call(tokens)
15
+ new(tokens).__send__(:run)
16
+ end
17
+
18
+ private
19
+
20
+ def error?
21
+ instance_variable_defined?(:@error)
22
+ end
23
+
24
+ def run
25
+ node = catch(:abort) do
26
+ parse_node.tap do
27
+ if next?
28
+ token = peek
29
+ error(
30
+ message: "Unexpected token: #{token.type}",
31
+ token: token
32
+ )
33
+ end
34
+ end
35
+ end
36
+
37
+ if error?
38
+ Either::Left.new(@error)
39
+ else
40
+ Either::Right.new(node)
41
+ end
42
+ end
43
+
44
+ def parse_node
45
+ structure = parse_node_type
46
+
47
+ attribute, descendant = nil
48
+
49
+ if optional(:properties_start)
50
+ loop do
51
+ break if optional(:properties_end)
52
+
53
+ name = expect(:string)
54
+
55
+ name_sym = name.value.to_sym
56
+
57
+ if structure.maybe_attribute(name_sym)
58
+ expect(:eq)
59
+ attribute = parse_attribute(name_sym)
60
+ next
61
+ end
62
+
63
+ if structure.maybe_descendant(name_sym)
64
+ expect(:eq)
65
+ descendant = parse_descendant(name_sym)
66
+ next
67
+ end
68
+
69
+ error(
70
+ message: "Node: #{structure.type} has no property named: #{name_sym}",
71
+ token: name
72
+ )
73
+ end
74
+ end
75
+
76
+ Node.new(
77
+ attribute: attribute,
78
+ descendant: descendant,
79
+ type: structure.type
80
+ )
81
+ end
82
+
83
+ def parse_attribute(name)
84
+ Node::Attribute.new(
85
+ name: name,
86
+ value: parse_alternative(
87
+ group_start: method(:parse_attribute_group),
88
+ string: method(:parse_attribute_value)
89
+ )
90
+ )
91
+ end
92
+
93
+ def parse_alternative(alternatives)
94
+ token = peek
95
+
96
+ alternatives.fetch(token.type) do
97
+ error(
98
+ message: "Expected one of: #{alternatives.keys.join(',')} but got: #{token.type}",
99
+ token: token
100
+ )
101
+ end.call
102
+ end
103
+
104
+ def parse_descendant(name)
105
+ Node::Descendant.new(
106
+ name: name,
107
+ pattern: parse_node
108
+ )
109
+ end
110
+
111
+ def parse_attribute_group
112
+ expect(:group_start)
113
+
114
+ values = []
115
+
116
+ loop do
117
+ values << parse_attribute_value
118
+ break unless optional(:delimiter)
119
+ end
120
+
121
+ expect(:group_end)
122
+
123
+ Node::Attribute::Value::Group.new(values: values)
124
+ end
125
+
126
+ def parse_attribute_value
127
+ Node::Attribute::Value::Single.new(value: expect(:string).value.to_sym)
128
+ end
129
+
130
+ def error(message:, token: nil)
131
+ @error =
132
+ if token
133
+ "#{message}\n#{token.display_location}"
134
+ else
135
+ message
136
+ end
137
+
138
+ throw(:abort)
139
+ end
140
+
141
+ def optional(type)
142
+ token = peek
143
+
144
+ return unless token&.type.equal?(type)
145
+
146
+ advance_position
147
+ token
148
+ end
149
+
150
+ def parse_node_type
151
+ token = expect(:string)
152
+
153
+ type = token.value.to_sym
154
+
155
+ Structure::ALL.fetch(type) do
156
+ error(token: token, message: "Expected valid node type got: #{type}")
157
+ end
158
+ end
159
+
160
+ def expect(type)
161
+ token = peek
162
+
163
+ unless token
164
+ error(message: "Expected token of type: #{type}, but got no token at all")
165
+ end
166
+
167
+ if token.type.eql?(type)
168
+ advance_position
169
+ token
170
+ else
171
+ error(
172
+ token: token,
173
+ message: "Expected token type: #{type} but got: #{token.type}"
174
+ )
175
+ end
176
+ end
177
+
178
+ def peek
179
+ @tokens.at(@next_position)
180
+ end
181
+
182
+ def advance_position
183
+ @next_position += 1
184
+ end
185
+
186
+ def next?
187
+ @next_position < @tokens.length
188
+ end
189
+ end
190
+ end # Pattern
191
+ # rubocop:enable Metrics/ClassLength
192
+ # rubocop:enable Metrics/MethodLength
193
+ end # AST
194
+ end # Mutant
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module AST
5
+ class Pattern
6
+ class Source
7
+ include Anima.new(:string)
8
+
9
+ def initialize(**attributes)
10
+ super
11
+
12
+ @lines = string.split("\n")
13
+ end
14
+
15
+ def line(line_index)
16
+ @lines.fetch(line_index)
17
+ end
18
+
19
+ class Location
20
+ include Anima.new(:source, :range, :line_index, :line_start)
21
+
22
+ def display
23
+ "#{source.line(line_index)}\n#{prefix}#{carets}"
24
+ end
25
+
26
+ private
27
+
28
+ def prefix
29
+ ' ' * (range.begin - line_start)
30
+ end
31
+
32
+ def carets
33
+ '^' * range.size
34
+ end
35
+ end
36
+ end # Source
37
+ end # Pattern
38
+ end # AST
39
+ end # Mutant
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module AST
5
+ class Pattern
6
+ class Token
7
+ include Anima.new(:type, :value, :location)
8
+
9
+ def display_location
10
+ location.display
11
+ end
12
+ end # Token
13
+ end # Pattern
14
+ end # AST
15
+ end # Mutant
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module AST
5
+ class Pattern
6
+ include Adamantium
7
+
8
+ def self.parse(syntax)
9
+ Lexer.call(syntax)
10
+ .lmap(&:display_message)
11
+ .bind(&Parser.public_method(:call))
12
+ end
13
+
14
+ class Node < self
15
+ include Anima.new(:type, :attribute, :descendant, :variable)
16
+
17
+ DEFAULTS = { attribute: nil, descendant: nil, variable: nil }.freeze
18
+
19
+ def initialize(attributes)
20
+ super(DEFAULTS.merge(attributes))
21
+ end
22
+
23
+ class Attribute
24
+ include Anima.new(:name, :value)
25
+
26
+ class Value
27
+ class Single < self
28
+ include Adamantium, Anima.new(:value)
29
+
30
+ def match?(input)
31
+ input.eql?(value)
32
+ end
33
+
34
+ def syntax
35
+ value
36
+ end
37
+ end
38
+
39
+ class Group < self
40
+ include Adamantium, Anima.new(:values)
41
+
42
+ def match?(value)
43
+ values.any? do |attribute_value|
44
+ attribute_value.match?(value)
45
+ end
46
+ end
47
+
48
+ def syntax
49
+ "(#{values.map(&:syntax).join(',')})"
50
+ end
51
+ end # Group
52
+ end # Value
53
+
54
+ def match?(node)
55
+ attribute = Structure.for(node.type).attribute(name) and value.match?(attribute.value(node))
56
+ end
57
+
58
+ def syntax
59
+ "#{name}=#{value.syntax}"
60
+ end
61
+ end # Attribute
62
+
63
+ class Descendant
64
+ include Anima.new(:name, :pattern)
65
+
66
+ def match?(node)
67
+ descendant = Structure.for(node.type).descendant(name).value(node)
68
+
69
+ !descendant.nil? && pattern.match?(descendant)
70
+ end
71
+
72
+ def syntax
73
+ "#{name}=#{pattern.syntax}"
74
+ end
75
+ end # Descendant
76
+
77
+ def match?(node)
78
+ fail NotImplementedError if variable
79
+
80
+ node.type.eql?(type) \
81
+ && (!attribute || attribute.match?(node)) \
82
+ && (!descendant || descendant.match?(node))
83
+ end
84
+
85
+ def syntax
86
+ "#{type}#{pair_syntax}"
87
+ end
88
+
89
+ private
90
+
91
+ def pair_syntax
92
+ pairs = [*attribute&.syntax, *descendant&.syntax]
93
+
94
+ return if pairs.empty?
95
+
96
+ "{#{pairs.join(' ')}}"
97
+ end
98
+ end
99
+
100
+ class Any < self
101
+ def match?(_node)
102
+ true
103
+ end
104
+ end
105
+
106
+ class None < self
107
+ def match?(_node)
108
+ false
109
+ end
110
+ end
111
+
112
+ class Deep < self
113
+ include Anima.new(:pattern)
114
+
115
+ def match?(node)
116
+ Structure.for(node.type).each_node(node) do |child|
117
+ return true if pattern.match?(child)
118
+ end
119
+
120
+ false
121
+ end
122
+ end
123
+ end # Pattern
124
+ end # AST
125
+ end # Mutant