mutant 0.11.10 → 0.11.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/mutant/ast/pattern/lexer.rb +171 -0
- data/lib/mutant/ast/pattern/parser.rb +194 -0
- data/lib/mutant/ast/pattern/source.rb +39 -0
- data/lib/mutant/ast/pattern/token.rb +15 -0
- data/lib/mutant/ast/pattern.rb +125 -0
- data/lib/mutant/ast/structure.rb +890 -0
- data/lib/mutant/mutator/node/regexp/capture_group.rb +3 -5
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/world.rb +1 -0
- data/lib/mutant.rb +7 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6aa7e42cfe0d8ed12281745fc607d4d933fbb03af10b2bb41c41e6df3765ef89
|
4
|
+
data.tar.gz: 2a4a940e72bf2381816cfc0522f236166df7c2f649bd4da6ee8c2f41a6851c33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ffa1cf3efbe20ba6ac20d7aee9765cbb8833a2934f2d1b874ec800673582c709e76bcbdb6a026191bd751a9f30726720c5f0f81b09e0999805608e22d775dc7
|
7
|
+
data.tar.gz: b4a2cf6f0fc16056e8f92b306828c4e5578f0b1408770b0395b4874ff887a3f1cbe5023613694493736bcdede6c906dfaed06c1b73e78f2b8b5821b2fb61457b
|
@@ -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,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
|