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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 163279d053e71f375fc6854b89e1ca0730d7de6a1cf883772097a635067fdc92
4
- data.tar.gz: c943e8ad5544a90d967ac9c580557a1e41997de4a2363cffe829710f0a71c8ae
3
+ metadata.gz: 6aa7e42cfe0d8ed12281745fc607d4d933fbb03af10b2bb41c41e6df3765ef89
4
+ data.tar.gz: 2a4a940e72bf2381816cfc0522f236166df7c2f649bd4da6ee8c2f41a6851c33
5
5
  SHA512:
6
- metadata.gz: 1a130b60a8e53fb84d6b95897bc2749007df58d114b8201ac409eb40f203ee9f90804df51f49083c345784d420335eaeaa884299e0c1d37634be6d72875b5580
7
- data.tar.gz: 18845045b045a1692c5574c092f7035560cab6267a8bd2a904266a69919630df59a47e0cc0d6edfc32717c834824b1b8d698da979aa7480359d363c1df9f1c1f
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,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