syntax_tree 0.1.0 → 1.0.0

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: e3977b8a9642c86ca56b63f93e541a7b9bdee08eef9d705f0540e4c1d6e95222
4
- data.tar.gz: b550f2a31561c3a06ca456005419a0743f5d62e60360797e70fa42cc075a603e
3
+ metadata.gz: c3684dd6ee3c63a0d12bd9688dcd5c31a508e5b56cbed0d9939843014dcb035c
4
+ data.tar.gz: 7b229ace8aa66b761b7a2d2b9ad6560a819e953b67d787ac3c9dc2cc224a36fd
5
5
  SHA512:
6
- metadata.gz: 1cab71763cb9cb537fb209fb175bc36074e58da9c7535d788911bb4d0e36a8ce28628eeae283d468d957c6c38549b33245dd7e6e07c749c725609fb53bd8ef5c
7
- data.tar.gz: b49ee91795fa64acd80d8cb5c244ff30fbbacc711da907c14f81f4f9cb7632c71cc3a6ae9d609224c4adcbdc54bfa89bb5f1b47ec17601653cdf059b8389feae
6
+ metadata.gz: 74efe38ca5ab0e3bfcd6fd8ac8809d88e0d4fc87d84178a188601c7afc50c06c18c85f2669f504f1d84635fb39a367fcbf3cdbdae1eb45a5bf79fc97f2447e78
7
+ data.tar.gz: 7b186fc7133a6f2806d73de62c15de8ae39682f7f317a7368f9a3a5877dc1d2f5f69824b08a0f970c8983f92fce8f21c824db20f9f46eecbb6786deacb46560a
data/CHANGELOG.md CHANGED
@@ -6,11 +6,82 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0]
10
+
11
+ ### Added
12
+
13
+ - The ability to "check" formatting by formatting the output of the first format.
14
+ - Comments can now be attached to the `case` keyword.
15
+ - Remove escaped forward slashes from regular expression literals when converting to `%r`.
16
+ - Allow arrays of `CHAR` nodes to be converted to `QWords` under certain conditions.
17
+ - Allow `HashLiteral` opening braces to have trailing comments.
18
+ - Add parentheses if `Yield` breaks onto multiple lines.
19
+ - Ensure all nodes that could have heredocs nested know about their end lines.
20
+ - Ensure comments on assignment after the `=` before the value keep their place.
21
+ - Trailing comments on parameters with no parentheses now do not force a break.
22
+ - Allow `ArrayLiteral` opening brackets to have trailing comments.
23
+ - Allow different line suffix nodes to have different priorities.
24
+ - Better support for encoding by properly reading encoding magic comments.
25
+ - Support singleton single-line method definitions.
26
+ - Support `stree-ignore` comments to ignore formatting nodes.
27
+ - Add special formatting for arrays of `VarRef` nodes whose sum width is greater than 2 * the maximum width.
28
+ - Better output formatting for the CLI.
29
+
30
+ ### Changed
31
+
32
+ - Force a break if a block is attached to a `Command` or `CommandCall` node.
33
+ - Don't indent `CommandCall` arguments if they don't fit aligned.
34
+ - Force a break in `Call` nodes if there are comments on the receiver.
35
+ - Do not change block bounds if inside of a `Command` or `CommandCall` node.
36
+ - Handle empty parentheses inside method calls.
37
+ - Skip indentation for special array literals on assignment nodes.
38
+ - Ensure a final breakable is inserted when converting an `ArrayLiteral` to a `QSymbols`.
39
+ - Fix up the `doc_width` calculation for `CommandCall` nodes.
40
+ - Ensure parameters inside a lambda literal when there are no parentheses are grouped.
41
+ - Ensure when converting an `ArrayLiteral` to a `QWords` that the strings do not contain `[`.
42
+ - Stop looking for parent `Command` or `CommandCall` nodes in blocks once you hit `Statements`.
43
+ - Ensure nested `Lambda` nodes get their correct bounds.
44
+ - Ensure we do not change block bounds within control flow constructs.
45
+ - Ensure parentheses are added around keywords changing to their modifier forms.
46
+ - Allow conditionals to take modifier form if they are using the `then` keyword with a `VoidStmt`.
47
+ - `UntilMod` and `WhileMod` nodes that wrap a `Begin` should be forced into their modifier forms.
48
+ - Ensure `For` loops keep their trailing commas.
49
+ - Replicate content for `__END__` keyword exactly.
50
+ - Keep block `If`, `Unless`, `While`, and `Until` forms if there is an assignment in the predicate.
51
+ - Force using braces if the block is within the predicate of a conditional or loop.
52
+ - Allow for the possibility that `CommandCall` nodes might not have arguments.
53
+ - Explicitly handle `?"` so that it formats properly.
54
+ - Check that a block is within the predicate in a more relaxed way.
55
+ - Ensure the `Return` breaks with brackets and not parentheses.
56
+ - Ensure trailing comments on parameter declarations are consistent.
57
+ - Make `Command` and `CommandCall` aware that their arguments could exceed their normal expected bounds because of heredocs.
58
+ - Only unescape forward slashes in regular expressions if converting from slash bounds to `%r` bounds.
59
+ - Allow `When` nodes to grab trailing comments away from their statements lists.
60
+ - Allow flip-flop operators to be formatted correctly within `IfMod` and `UnlessMod` nodes.
61
+ - Allow `IfMod` and `UnlessMod` to know about heredocs moving their bounds.
62
+ - Properly handle breaking parameters when there are no parentheses.
63
+ - Properly handle trailing operators in call chains with attached comments.
64
+ - Force using braces if the block is within the predicate of a ternary.
65
+ - Properly handle trailing comments after a `then` operator on a `When` or `In` clause.
66
+ - Ensure nested `HshPtn` nodes use braces.
67
+ - Force using braces if the block is within a `Binary` within the predicate of a loop or conditional.
68
+ - Make sure `StringLiteral` and `StringEmbExpr` know that they can be extended by heredocs.
69
+ - Ensure `Int` nodes with preceding unary `+` get formatted properly.
70
+ - Properly handle byte-order mark column offsets at the beginnings of files.
71
+ - Ensure `Words`, `Symbols`, `QWords`, and `QSymbols` properly format when their contents contain brackets.
72
+ - Ensure ternaries being broken out into `if`...`else`...`end` get wrapped in parentheses if necessary.
73
+
74
+ ### Removed
75
+
76
+ - The `AccessCtrl` node in favor of just formatting correctly when you hit a `Statements` node.
77
+ - The `MethodAddArg` node is removed in favor of an optional `arguments` field on `Call` and `FCall`.
78
+
9
79
  ## [0.1.0] - 2021-11-16
10
80
 
11
81
  ### Added
12
82
 
13
83
  - 🎉 Initial release! 🎉
14
84
 
15
- [unreleased]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...HEAD
85
+ [unreleased]: https://github.com/kddnewton/syntax_tree/compare/v1.0.0...HEAD
86
+ [1.0.0]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...v1.0.0
16
87
  [0.1.0]: https://github.com/kddnewton/syntax_tree/compare/8aa1f5...v0.1.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (0.1.0)
4
+ syntax_tree (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -10,7 +10,7 @@ GEM
10
10
  benchmark-ips (2.9.2)
11
11
  docile (1.4.0)
12
12
  minitest (5.14.4)
13
- parser (3.0.3.1)
13
+ parser (3.0.3.2)
14
14
  ast (~> 2.4.1)
15
15
  rake (13.0.6)
16
16
  ruby_parser (3.18.1)
data/README.md CHANGED
@@ -49,6 +49,13 @@ class MyClass
49
49
  ...
50
50
  ```
51
51
 
52
+ or
53
+
54
+ ```sh
55
+ $ stree write program.rb
56
+ program.rb 1ms
57
+ ```
58
+
52
59
  ## Development
53
60
 
54
61
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/exe/stree CHANGED
@@ -1,84 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative File.expand_path("../lib/syntax_tree", __dir__)
4
+ $:.unshift(File.expand_path("../lib", __dir__))
5
+ require "syntax_tree"
6
+ require "syntax_tree/cli"
5
7
 
6
- help = <<~EOF
7
- stree MDOE FILE
8
-
9
- MODE: one of "a", "ast", "d", "doc", "f", "format", "w", or "write"
10
- FILE: one or more paths to files to parse
11
- EOF
12
-
13
- if ARGV.length < 2
14
- warn(help)
15
- exit(1)
16
- end
17
-
18
- module SyntaxTree::CLI
19
- class AST
20
- def run(filepath)
21
- pp SyntaxTree.parse(File.read(filepath))
22
- end
23
- end
24
-
25
- class Doc
26
- def run(filepath)
27
- formatter = SyntaxTree::Formatter.new([])
28
- SyntaxTree.parse(File.read(filepath)).format(formatter)
29
- pp formatter.groups.first
30
- end
31
- end
32
-
33
- class Format
34
- def run(filepath)
35
- puts SyntaxTree.format(File.read(filepath))
36
- end
37
- end
38
-
39
- class Write
40
- def run(filepath)
41
- File.write(filepath, SyntaxTree.format(File.read(filepath)))
42
- end
43
- end
44
- end
45
-
46
- mode =
47
- case ARGV.shift
48
- when "a", "ast"
49
- SyntaxTree::CLI::AST.new
50
- when "d", "doc"
51
- SyntaxTree::CLI::Doc.new
52
- when "f", "format"
53
- SyntaxTree::CLI::Format.new
54
- when "w", "write"
55
- SyntaxTree::CLI::Write.new
56
- else
57
- warn(help)
58
- exit(1)
59
- end
60
-
61
- queue = Queue.new
62
- ARGV.each { |pattern| Dir[pattern].each { |filepath| queue << filepath } }
63
-
64
- if queue.size <= 1
65
- filepath = queue.shift
66
- mode.run(filepath) if File.file?(filepath)
67
- return
68
- end
69
-
70
- count = [8, queue.size].min
71
- threads =
72
- count.times.map do
73
- Thread.new do
74
- loop do
75
- filepath = queue.shift
76
- break if filepath == :exit
77
-
78
- mode.run(filepath) if File.file?(filepath)
79
- end
80
- end
81
- end
82
-
83
- count.times { queue << :exit }
84
- threads.each(&:join)
8
+ exit(SyntaxTree::CLI.run(ARGV))
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SyntaxTree
4
+ module CLI
5
+ # A utility wrapper around colored strings in the output.
6
+ class Color
7
+ attr_reader :value, :code
8
+
9
+ def initialize(value, code)
10
+ @value = value
11
+ @code = code
12
+ end
13
+
14
+ def to_s
15
+ "\033[#{code}m#{value}\033[0m"
16
+ end
17
+
18
+ def self.gray(value)
19
+ new(value, "38;5;102")
20
+ end
21
+
22
+ def self.red(value)
23
+ new(value, "1;31")
24
+ end
25
+
26
+ def self.yellow(value)
27
+ new(value, "33")
28
+ end
29
+ end
30
+
31
+ # The parent action class for the CLI that implements the basics.
32
+ class Action
33
+ def run(filepath, source)
34
+ end
35
+
36
+ def success
37
+ end
38
+
39
+ def failure
40
+ end
41
+ end
42
+
43
+ # An action of the CLI that prints out the AST for the given source.
44
+ class AST < Action
45
+ def run(filepath, source)
46
+ pp SyntaxTree.parse(source)
47
+ end
48
+ end
49
+
50
+ # An action of the CLI that ensures that the filepath is formatted as
51
+ # expected.
52
+ class Check < Action
53
+ class UnformattedError < StandardError
54
+ end
55
+
56
+ def run(filepath, source)
57
+ raise UnformattedError if source != SyntaxTree.format(source)
58
+ rescue
59
+ warn("[#{Color.yellow("warn")}] #{filepath}")
60
+ raise
61
+ end
62
+
63
+ def success
64
+ puts("All files matched expected format.")
65
+ end
66
+
67
+ def failure
68
+ warn("The listed files did not match the expected format.")
69
+ end
70
+ end
71
+
72
+ # An action of the CLI that formats the source twice to check if the first
73
+ # format is not idempotent.
74
+ class Debug < Action
75
+ class NonIdempotentFormatError < StandardError
76
+ end
77
+
78
+ def run(filepath, source)
79
+ warning = "[#{Color.yellow("warn")}] #{filepath}"
80
+ formatted = SyntaxTree.format(source)
81
+
82
+ if formatted != SyntaxTree.format(formatted)
83
+ raise NonIdempotentFormatError
84
+ end
85
+ rescue
86
+ warn(warning)
87
+ raise
88
+ end
89
+
90
+ def success
91
+ puts("All files can be formatted idempotently.")
92
+ end
93
+
94
+ def failure
95
+ warn("The listed files could not be formatted idempotently.")
96
+ end
97
+ end
98
+
99
+ # An action of the CLI that prints out the doc tree IR for the given source.
100
+ class Doc < Action
101
+ def run(filepath, source)
102
+ formatter = Formatter.new([])
103
+ SyntaxTree.parse(source).format(formatter)
104
+ pp formatter.groups.first
105
+ end
106
+ end
107
+
108
+ # An action of the CLI that formats the input source and prints it out.
109
+ class Format < Action
110
+ def run(filepath, source)
111
+ puts SyntaxTree.format(source)
112
+ end
113
+ end
114
+
115
+ # An action of the CLI that formats the input source and writes the
116
+ # formatted output back to the file.
117
+ class Write < Action
118
+ def run(filepath, source)
119
+ print filepath
120
+ start = Time.now
121
+
122
+ formatted = SyntaxTree.format(source)
123
+ File.write(filepath, formatted)
124
+
125
+ color = source == formatted ? Color.gray(filepath) : filepath
126
+ delta = ((Time.now - start) * 1000).round
127
+
128
+ puts "\r#{color} #{delta}ms"
129
+ end
130
+ end
131
+
132
+ # The help message displayed if the input arguments are not correctly
133
+ # ordered or formatted.
134
+ HELP = <<~HELP
135
+ stree MODE FILE
136
+
137
+ MODE: ast | check | debug | doc | format | write
138
+ FILE: one or more paths to files to parse
139
+ HELP
140
+
141
+ class << self
142
+ # Run the CLI over the given array of strings that make up the arguments
143
+ # passed to the invocation.
144
+ def run(argv)
145
+ if argv.length < 2
146
+ warn(HELP)
147
+ return 1
148
+ end
149
+
150
+ arg, *patterns = argv
151
+ action =
152
+ case arg
153
+ when "a", "ast"
154
+ AST.new
155
+ when "c", "check"
156
+ Check.new
157
+ when "debug"
158
+ Debug.new
159
+ when "doc"
160
+ Doc.new
161
+ when "f", "format"
162
+ Format.new
163
+ when "w", "write"
164
+ Write.new
165
+ else
166
+ warn(HELP)
167
+ return 1
168
+ end
169
+
170
+ errored = false
171
+ patterns.each do |pattern|
172
+ Dir.glob(pattern).each do |filepath|
173
+ next unless File.file?(filepath)
174
+ source = source_for(filepath)
175
+
176
+ begin
177
+ action.run(filepath, source)
178
+ rescue ParseError => error
179
+ warn("Error: #{error.message}")
180
+ lines = source.lines
181
+
182
+ maximum = [error.lineno + 3, lines.length].min
183
+ digits = Math.log10(maximum).ceil
184
+
185
+ ([error.lineno - 3, 0].max...maximum).each do |line_index|
186
+ line_number = line_index + 1
187
+
188
+ if line_number == error.lineno
189
+ part1 = Color.red(">")
190
+ part2 = Color.gray("%#{digits}d |" % line_number)
191
+ warn("#{part1} #{part2} #{lines[line_index]}")
192
+
193
+ part3 = Color.gray(" %#{digits}s |" % " ")
194
+ warn("#{part3} #{" " * error.column}#{Color.red("^")}")
195
+ else
196
+ prefix = Color.gray(" %#{digits}d |" % line_number)
197
+ warn("#{prefix} #{lines[line_index]}")
198
+ end
199
+ end
200
+
201
+ errored = true
202
+ rescue Check::UnformattedError, Debug::NonIdempotentFormatError
203
+ errored = true
204
+ rescue => error
205
+ warn(error.message)
206
+ warn(error.backtrace)
207
+ errored = true
208
+ end
209
+ end
210
+ end
211
+
212
+ if errored
213
+ action.failure
214
+ 1
215
+ else
216
+ action.success
217
+ 0
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ # Returns the source from the given filepath taking into account any
224
+ # potential magic encoding comments.
225
+ def source_for(filepath)
226
+ encoding =
227
+ File.open(filepath, "r") do |file|
228
+ header = file.readline
229
+ header += file.readline if header.start_with?("#!")
230
+ Ripper.new(header).tap(&:parse).encoding
231
+ end
232
+
233
+ File.read(filepath, encoding: encoding)
234
+ end
235
+ end
236
+ end
237
+ end
@@ -213,9 +213,12 @@ class PrettyPrint
213
213
  # constantly check where the line ends to avoid accidentally printing some
214
214
  # content after a line suffix node.
215
215
  class LineSuffix
216
- attr_reader :contents
216
+ DEFAULT_PRIORITY = 1
217
217
 
218
- def initialize(contents: [])
218
+ attr_reader :priority, :contents
219
+
220
+ def initialize(priority: DEFAULT_PRIORITY, contents: [])
221
+ @priority = priority
219
222
  @contents = contents
220
223
  end
221
224
 
@@ -741,10 +744,17 @@ class PrettyPrint
741
744
 
742
745
  # This is a separate command stack that includes the same kind of triplets
743
746
  # as the commands variable. It is used to keep track of things that should
744
- # go at the end of printed lines once the other doc nodes are
745
- # accounted for. Typically this is used to implement comments.
747
+ # go at the end of printed lines once the other doc nodes are accounted for.
748
+ # Typically this is used to implement comments.
746
749
  line_suffixes = []
747
750
 
751
+ # This is a special sort used to order the line suffixes by both the
752
+ # priority set on the line suffix and the index it was in the original
753
+ # array.
754
+ line_suffix_sort = ->(line_suffix) do
755
+ [-line_suffix.last, -line_suffixes.index(line_suffix)]
756
+ end
757
+
748
758
  # This is a linear stack instead of a mutually recursive call defined on
749
759
  # the individual doc nodes for efficiency.
750
760
  while commands.any?
@@ -783,7 +793,7 @@ class PrettyPrint
783
793
  commands << [indent, mode, doc.flat_contents] if doc.flat_contents
784
794
  end
785
795
  when LineSuffix
786
- line_suffixes << [indent, mode, doc.contents]
796
+ line_suffixes << [indent, mode, doc.contents, doc.priority]
787
797
  when Breakable
788
798
  if mode == MODE_FLAT
789
799
  if doc.force?
@@ -804,7 +814,7 @@ class PrettyPrint
804
814
  # to flush them now, as we are about to add a newline.
805
815
  if line_suffixes.any?
806
816
  commands << [indent, mode, doc]
807
- commands += line_suffixes.reverse
817
+ commands += line_suffixes.sort_by(&line_suffix_sort)
808
818
  line_suffixes = []
809
819
  next
810
820
  end
@@ -838,7 +848,7 @@ class PrettyPrint
838
848
  end
839
849
 
840
850
  if commands.empty? && line_suffixes.any?
841
- commands += line_suffixes.reverse
851
+ commands += line_suffixes.sort_by(&line_suffix_sort)
842
852
  line_suffixes = []
843
853
  end
844
854
  end
@@ -1012,8 +1022,8 @@ class PrettyPrint
1012
1022
 
1013
1023
  # Inserts a LineSuffix node into the print tree. The contents of the node are
1014
1024
  # determined by the block.
1015
- def line_suffix
1016
- doc = LineSuffix.new
1025
+ def line_suffix(priority: LineSuffix::DEFAULT_PRIORITY)
1026
+ doc = LineSuffix.new(priority: priority)
1017
1027
  target << doc
1018
1028
 
1019
1029
  with_target(doc.contents) { yield }
@@ -3,5 +3,5 @@
3
3
  require "ripper"
4
4
 
5
5
  class SyntaxTree < Ripper
6
- VERSION = "0.1.0"
6
+ VERSION = "1.0.0"
7
7
  end