unparser 0.4.7 → 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  unparser
2
2
  ========
3
3
 
4
- [![Build Status](https://secure.travis-ci.org/mbj/unparser.svg?branch=master)](http://travis-ci.org/mbj/unparser)
5
- [![Code Climate](https://codeclimate.com/github/mbj/unparser.svg)](https://codeclimate.com/github/mbj/unparser)
4
+ [![CI](https://github.com/mbj/unparser/workflows/CI/badge.svg)
6
5
  [![Gem Version](https://img.shields.io/gem/v/unparser.svg)](https://rubygems.org/gems/unparser)
7
6
 
8
7
  Generate equivalent source for ASTs from whitequarks [parser](https://github.com/whitequark/parser).
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
3
  require 'abstract_type'
5
- require 'procto'
4
+ require 'anima'
6
5
  require 'concord'
6
+ require 'diff/lcs'
7
+ require 'diff/lcs/hunk'
8
+ require 'mprelude'
7
9
  require 'parser/current'
10
+ require 'procto'
11
+ require 'set'
8
12
 
9
13
  # Library namespace
10
14
  module Unparser
@@ -40,11 +44,22 @@ module Unparser
40
44
  #
41
45
  # @param [String] source
42
46
  #
43
- # @return [Parser::AST::Node]
47
+ # @return [Parser::AST::Node, nil]
44
48
  def self.parse(source)
45
49
  parser.parse(buffer(source))
46
50
  end
47
51
 
52
+ # Parse string into either syntax error or AST
53
+ #
54
+ # @param [String] source
55
+ #
56
+ # @return [MPrelude::Either<Parser::SyntaxError, (Parser::ASTNode, nil)>]
57
+ def self.parse_either(source)
58
+ MPrelude::Either.wrap_error(Parser::SyntaxError) do
59
+ parser.parse(buffer(source))
60
+ end
61
+ end
62
+
48
63
  # Parse string into AST, with comments
49
64
  #
50
65
  # @param [String] source
@@ -100,6 +115,8 @@ require 'unparser/constants'
100
115
  require 'unparser/dsl'
101
116
  require 'unparser/ast'
102
117
  require 'unparser/ast/local_variable_scope'
118
+ require 'unparser/diff'
119
+ require 'unparser/color'
103
120
  require 'unparser/emitter'
104
121
  require 'unparser/emitter/literal'
105
122
  require 'unparser/emitter/literal/primitive'
@@ -152,5 +169,6 @@ require 'unparser/emitter/resbody'
152
169
  require 'unparser/emitter/ensure'
153
170
  require 'unparser/emitter/index'
154
171
  require 'unparser/emitter/lambda'
172
+ require 'unparser/validation'
155
173
  # make it easy for zombie
156
174
  require 'unparser/finalize'
@@ -2,12 +2,6 @@
2
2
 
3
3
  require 'unparser'
4
4
  require 'optparse'
5
- require 'diff/lcs'
6
- require 'diff/lcs/hunk'
7
-
8
- require 'unparser/cli/source'
9
- require 'unparser/cli/differ'
10
- require 'unparser/cli/color'
11
5
 
12
6
  module Unparser
13
7
  # Unparser CLI implementation
@@ -19,6 +13,36 @@ module Unparser
19
13
  EXIT_SUCCESS = 0
20
14
  EXIT_FAILURE = 1
21
15
 
16
+ class Target
17
+ include AbstractType
18
+
19
+ # Path target
20
+ class Path < self
21
+ include Concord.new(:path)
22
+
23
+ # Validation for this target
24
+ #
25
+ # @return [Validation]
26
+ def validation
27
+ Validation.from_path(path)
28
+ end
29
+ end
30
+
31
+ # String target
32
+ class String
33
+ include Concord.new(:string)
34
+
35
+ # Validation for this target
36
+ #
37
+ # @return [Validation]
38
+ def validation
39
+ Validation.from_string(string)
40
+ end
41
+ end # String
42
+ end # Target
43
+
44
+ private_constant(*constants(false))
45
+
22
46
  # Run CLI
23
47
  #
24
48
  # @param [Array<String>] arguments
@@ -42,8 +66,8 @@ module Unparser
42
66
  #
43
67
  # ignore :reek:TooManyStatements
44
68
  def initialize(arguments)
45
- @sources = []
46
- @ignore = Set.new
69
+ @ignore = Set.new
70
+ @targets = []
47
71
 
48
72
  @success = true
49
73
  @fail_fast = false
@@ -54,7 +78,7 @@ module Unparser
54
78
  end
55
79
 
56
80
  opts.parse!(arguments).each do |name|
57
- @sources.concat(sources(name))
81
+ @targets.concat(targets(name))
58
82
  end
59
83
  end
60
84
 
@@ -71,16 +95,16 @@ module Unparser
71
95
  builder.banner = 'usage: unparse [options] FILE [FILE]'
72
96
  builder.separator('')
73
97
  builder.on('-e', '--evaluate SOURCE') do |source|
74
- @sources << Source::String.new(source)
98
+ @targets << Target::String.new(source)
75
99
  end
76
- builder.on('--start-with FILE') do |file|
77
- @start_with = sources(file).first
100
+ builder.on('--start-with FILE') do |path|
101
+ @start_with = targets(path).first
78
102
  end
79
103
  builder.on('-v', '--verbose') do
80
104
  @verbose = true
81
105
  end
82
106
  builder.on('--ignore FILE') do |file|
83
- @ignore.merge(sources(file))
107
+ @ignore.merge(targets(file))
84
108
  end
85
109
  builder.on('--fail-fast') do
86
110
  @fail_fast = true
@@ -94,10 +118,8 @@ module Unparser
94
118
  # @api private
95
119
  #
96
120
  def exit_status
97
- effective_sources.each do |source|
98
- next if @ignore.include?(source)
99
-
100
- process_source(source)
121
+ effective_targets.each do |target|
122
+ process_target(target)
101
123
  break if @fail_fast && !@success
102
124
  end
103
125
 
@@ -106,66 +128,64 @@ module Unparser
106
128
 
107
129
  private
108
130
 
109
- # Process source
131
+ # Process target
110
132
  #
111
- # @param [CLI::Source] source
133
+ # @param [Target] target
112
134
  #
113
135
  # @return [undefined]
114
136
  #
115
137
  # @api private
116
138
  #
117
- def process_source(source)
118
- if source.success?
119
- puts source.report if @verbose
120
- puts "Success: #{source.identification}"
139
+ def process_target(target)
140
+ validation = target.validation
141
+ if validation.success?
142
+ puts validation.report if @verbose
143
+ puts "Success: #{validation.identification}"
121
144
  else
122
- puts source.report
123
- puts "Error: #{source.identification}"
145
+ puts validation.report
146
+ puts "Error: #{validation.identification}"
124
147
  @success = false
125
148
  end
126
149
  end
127
150
 
128
- # Return effective sources
151
+ # Return effective targets
129
152
  #
130
- # @return [Enumerable<CLI::Source>]
153
+ # @return [Enumerable<Target>]
131
154
  #
132
155
  # @api private
133
156
  #
134
- def effective_sources
157
+ def effective_targets
135
158
  if @start_with
136
159
  reject = true
137
- @sources.reject do |source|
138
- if reject && source.eql?(@start_with)
160
+ @targets.reject do |targets|
161
+ if reject && targets.eql?(@start_with)
139
162
  reject = false
140
163
  end
141
164
 
142
165
  reject
143
166
  end
144
167
  else
145
- @sources
146
- end
168
+ @targets
169
+ end.reject(&@ignore.method(:include?))
147
170
  end
148
171
 
149
- # Return sources for file name
172
+ # Return targets for file name
150
173
  #
151
174
  # @param [String] file_name
152
175
  #
153
- # @return [Enumerable<CLI::Source>]
176
+ # @return [Enumerable<Target>]
154
177
  #
155
178
  # @api private
156
179
  #
157
180
  # ignore :reek:UtilityFunction
158
- def sources(file_name)
159
- files =
160
- if File.directory?(file_name)
161
- Dir.glob(File.join(file_name, '**/*.rb')).sort
162
- elsif File.file?(file_name)
163
- [file_name]
164
- else
165
- Dir.glob(file_name).sort
166
- end
167
-
168
- files.map(&Source::File.method(:new))
181
+ def targets(file_name)
182
+ if File.directory?(file_name)
183
+ Dir.glob(File.join(file_name, '**/*.rb')).sort
184
+ elsif File.file?(file_name)
185
+ [file_name]
186
+ else
187
+ Dir.glob(file_name).sort
188
+ end.map { |file| Target::Path.new(Pathname.new(file)) }
169
189
  end
170
190
 
171
191
  end # CLI
@@ -10,9 +10,6 @@ module Unparser
10
10
  # @param [String] text
11
11
  #
12
12
  # @return [String]
13
- #
14
- # @api private
15
- #
16
13
  def format(text)
17
14
  "\e[#{code}m#{text}\e[0m"
18
15
  end
@@ -25,9 +22,6 @@ module Unparser
25
22
  #
26
23
  # @return [String]
27
24
  # the argument string
28
- #
29
- # @api private
30
- #
31
25
  def format(text)
32
26
  text
33
27
  end
@@ -37,16 +31,12 @@ module Unparser
37
31
  # Initialize null color
38
32
  #
39
33
  # @return [undefined]
40
- #
41
- # @api private
42
- #
43
34
  def initialize; end
44
35
 
45
36
  end.new
46
37
 
47
38
  RED = Color.new(31)
48
39
  GREEN = Color.new(32)
49
- BLUE = Color.new(34)
50
40
 
51
41
  end # Color
52
42
  end # Unparser
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ # Class to create diffs from source code
5
+ class Diff
6
+ include Adamantium::Flat, Concord.new(:old, :new)
7
+
8
+ ADDITION = '+'
9
+ DELETION = '-'
10
+ NEWLINE = "\n"
11
+
12
+ # Unified source diff between old and new
13
+ #
14
+ # @return [String]
15
+ # if there is exactly one diff
16
+ #
17
+ # @return [nil]
18
+ # otherwise
19
+ def diff
20
+ return if diffs.empty?
21
+
22
+ minimized_hunk.diff(:unified) + NEWLINE
23
+ end
24
+ memoize :diff
25
+
26
+ # Colorized unified source diff between old and new
27
+ #
28
+ # @return [String]
29
+ # if there is a diff
30
+ #
31
+ # @return [nil]
32
+ # otherwise
33
+ def colorized_diff
34
+ return unless diff
35
+
36
+ diff.lines.map(&self.class.method(:colorize_line)).join
37
+ end
38
+ memoize :colorized_diff
39
+
40
+ # Build new object from source strings
41
+ #
42
+ # @param [String] old
43
+ # @param [String] new
44
+ #
45
+ # @return [Diff]
46
+ def self.build(old, new)
47
+ new(lines(old), lines(new))
48
+ end
49
+
50
+ # Break up source into lines
51
+ #
52
+ # @param [String] source
53
+ #
54
+ # @return [Array<String>]
55
+ def self.lines(source)
56
+ source.lines.map(&:chomp)
57
+ end
58
+ private_class_method :lines
59
+
60
+ private
61
+
62
+ # Diffs between old and new
63
+ #
64
+ # @return [Array<Array>]
65
+ def diffs
66
+ ::Diff::LCS.diff(old, new)
67
+ end
68
+
69
+ # Raw diff-lcs hunks
70
+ #
71
+ # @return [Array<Diff::LCS::Hunk>]
72
+ def hunks
73
+ diffs.map do |diff|
74
+ ::Diff::LCS::Hunk.new(old.map(&:dup), new, diff, max_length, 0)
75
+ end
76
+ end
77
+
78
+ # Minimized hunk
79
+ #
80
+ # @return Diff::LCS::Hunk
81
+ def minimized_hunk
82
+ head, *tail = hunks
83
+
84
+ tail.reduce(head) do |left, right|
85
+ right.merge(left)
86
+ right
87
+ end
88
+ end
89
+
90
+ # Max length of source line in new and old
91
+ #
92
+ # @return [Integer]
93
+ def max_length
94
+ [old, new].map(&:length).max
95
+ end
96
+
97
+ # Colorized a unified diff line
98
+ #
99
+ # @param [String] line
100
+ #
101
+ # @return [String]
102
+ def self.colorize_line(line)
103
+ case line[0]
104
+ when ADDITION
105
+ Color::GREEN
106
+ when DELETION
107
+ Color::RED
108
+ else
109
+ Color::NONE
110
+ end.format(line)
111
+ end
112
+ private_class_method :colorize_line
113
+
114
+ end # Diff
115
+ end # Unparser
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unparser
4
+ UnknownNodeError = Class.new(ArgumentError)
4
5
 
5
6
  # Emitter base class
6
7
  #
@@ -116,7 +117,7 @@ module Unparser
116
117
  def self.emitter(node, parent)
117
118
  type = node.type
118
119
  klass = REGISTRY.fetch(type) do
119
- raise ArgumentError, "No emitter for node: #{type.inspect}"
120
+ raise UnknownNodeError, "Unknown node type: #{type.inspect}"
120
121
  end
121
122
  klass.new(node, parent)
122
123
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ # Validation of unparser results
5
+ #
6
+ # ignore :reek:TooManyMethods
7
+ class Validation
8
+ include Adamantium::Flat, Anima.new(
9
+ :generated_node,
10
+ :generated_source,
11
+ :identification,
12
+ :original_node,
13
+ :original_source
14
+ )
15
+
16
+ # Test if source could be unparsed successfully
17
+ #
18
+ # @return [Boolean]
19
+ #
20
+ # @api private
21
+ #
22
+ def success?
23
+ [
24
+ original_source,
25
+ original_node,
26
+ generated_source,
27
+ generated_node
28
+ ].all?(&:right?) && generated_node.from_right.eql?(original_node.from_right)
29
+ end
30
+
31
+ # Return error report
32
+ #
33
+ # @return [String]
34
+ #
35
+ # @api private
36
+ #
37
+ def report
38
+ message = [identification]
39
+
40
+ message.concat(make_report('Original-Source', :original_source))
41
+ message.concat(make_report('Generated-Source', :generated_source))
42
+ message.concat(make_report('Original-Node', :original_node))
43
+ message.concat(make_report('Generated-Node', :generated_node))
44
+ message.concat(node_diff_report)
45
+
46
+ message.join("\n")
47
+ end
48
+ memoize :report
49
+
50
+ # Create validator from string
51
+ #
52
+ # @param [String] original_source
53
+ #
54
+ # @return [Validator]
55
+ def self.from_string(original_source)
56
+ original_node = Unparser
57
+ .parse_either(original_source)
58
+ .fmap(&Preprocessor.method(:run))
59
+
60
+ generated_source = original_node
61
+ .lmap(&method(:const_unit))
62
+ .bind(&method(:unparse_either))
63
+
64
+ generated_node = generated_source
65
+ .lmap(&method(:const_unit))
66
+ .bind(&Unparser.method(:parse_either))
67
+ .fmap(&Preprocessor.method(:run))
68
+
69
+ new(
70
+ identification: '(string)',
71
+ original_source: MPrelude::Either::Right.new(original_source),
72
+ original_node: original_node,
73
+ generated_source: generated_source,
74
+ generated_node: generated_node
75
+ )
76
+ end
77
+
78
+ # Create validator from file
79
+ #
80
+ # @param [Pathname] path
81
+ #
82
+ # @return [Validator]
83
+ def self.from_path(path)
84
+ from_string(path.read).with(identification: path.to_s)
85
+ end
86
+
87
+ private
88
+
89
+ # Create a labeled report from
90
+ #
91
+ # @param [String] label
92
+ # @param [Symbol] attribute_name
93
+ #
94
+ # @return [Array<String>]
95
+ def make_report(label, attribute_name)
96
+ ["#{label}:"].concat(public_send(attribute_name).either(method(:report_exception), ->(value) { [value] }))
97
+ end
98
+
99
+ # Report optional exception
100
+ #
101
+ # @param [Exception, nil] exception
102
+ #
103
+ # @return [Array<String>]
104
+ def report_exception(exception)
105
+ if exception
106
+ [exception.inspect].concat(exception.backtrace.take(20))
107
+ else
108
+ ['undefined']
109
+ end
110
+ end
111
+
112
+ # Report the node diff
113
+ #
114
+ # @return [Array<String>]
115
+ def node_diff_report
116
+ diff = nil
117
+
118
+ original_node.fmap do |original|
119
+ generated_node.fmap do |generated|
120
+ diff = Diff.new(
121
+ original.to_s.lines.map(&:chomp),
122
+ generated.to_s.lines.map(&:chomp)
123
+ ).colorized_diff
124
+ end
125
+ end
126
+
127
+ diff ? ['Node-Diff:', diff] : []
128
+ end
129
+
130
+ # Create unit represented as nil
131
+ #
132
+ # @param [Object] _value
133
+ #
134
+ # @return [nil]
135
+ def self.const_unit(_value); end
136
+ private_class_method :const_unit
137
+
138
+ # Unparse capturing errors
139
+ #
140
+ # @param [Parser::AST::Node] node
141
+ #
142
+ # @return [Either<RuntimeError, String>]
143
+ def self.unparse_either(node)
144
+ MPrelude::Either
145
+ .wrap_error(RuntimeError) { Unparser.unparse(node) }
146
+ end
147
+ private_class_method :unparse_either
148
+ end # Validation
149
+ end # Unparser