mutant 0.11.10 → 0.11.11
Sign up to get free protection for your applications and to get access to all the features.
- 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
|