unparser 0.4.7 → 0.4.8

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.
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