syntax_tree 0.1.0 → 1.0.0

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