syntax_tree 0.1.0 → 1.2.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: 4797ffd100da9b050f154cd0ac4a235b342c395e49747ee51574eb0e0f9fcf1a
4
+ data.tar.gz: 52babf707ae1bf42abaeb51b350644fc50f61fa1b74482abb776e863d2ec48dd
5
5
  SHA512:
6
- metadata.gz: 1cab71763cb9cb537fb209fb175bc36074e58da9c7535d788911bb4d0e36a8ce28628eeae283d468d957c6c38549b33245dd7e6e07c749c725609fb53bd8ef5c
7
- data.tar.gz: b49ee91795fa64acd80d8cb5c244ff30fbbacc711da907c14f81f4f9cb7632c71cc3a6ae9d609224c4adcbdc54bfa89bb5f1b47ec17601653cdf059b8389feae
6
+ metadata.gz: 62e28b7e6b25e81bdfb70817c02152bd640395149c1ac75fa553154ebb636c9c913dbf2ebda7a65f9687dd1bb69d51e0e3d8f5ffba172d0b1eadb5849e64369a
7
+ data.tar.gz: 6233b01960317929ce137305a08f45d3d756d741b5f6b6d037f2499377a53922f222543f433912cc853ee31726e2306527fbe29c75ad757e6125207b7476956c
@@ -13,7 +13,7 @@ jobs:
13
13
  - uses: ruby/setup-ruby@v1
14
14
  with:
15
15
  bundler-cache: true
16
- ruby-version: 3.0
16
+ ruby-version: '3.1'
17
17
  - name: Test
18
18
  run: bundle exec rake test
19
19
  automerge:
data/CHANGELOG.md CHANGED
@@ -6,11 +6,108 @@ 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.2.0] - 2022-01-09
10
+
11
+ ### Added
12
+
13
+ - Support for Ruby 3.1 syntax, including: blocks without names, hash keys without values, endless methods without parentheses, and new argument forwarding.
14
+ - Support for pinned expressions and variables within pattern matching.
15
+ - Support endless ranges as the final argument to a `when` clause.
16
+
17
+ ## [1.1.1] - 2021-12-09
18
+
19
+ ### Added
20
+
21
+ - [#7](https://github.com/kddnewton/syntax_tree/issues/7) Better formatting for hashes and arrays that are values in hashes.
22
+ - [#9](https://github.com/kddnewton/syntax_tree/issues/9) Special handling for RSpec matchers when nesting `CommandCall` nodes.
23
+ - [#10](https://github.com/kddnewton/syntax_tree/issues/10) Force the maintaining of the modifier forms of conditionals and loops if the statement includes an assignment. Also, for the maintaining of the block form of conditionals and loops if the predicate includes an assignment.
24
+
25
+ ## [1.1.0] - 2021-12-08
26
+
27
+ ### Added
28
+
29
+ - Better handling for formatting files with errors.
30
+ - Colorize the output snippet using IRB.
31
+
32
+ ## [1.0.0] - 2021-12-08
33
+
34
+ ### Added
35
+
36
+ - The ability to "check" formatting by formatting the output of the first format.
37
+ - Comments can now be attached to the `case` keyword.
38
+ - Remove escaped forward slashes from regular expression literals when converting to `%r`.
39
+ - Allow arrays of `CHAR` nodes to be converted to `QWords` under certain conditions.
40
+ - Allow `HashLiteral` opening braces to have trailing comments.
41
+ - Add parentheses if `Yield` breaks onto multiple lines.
42
+ - Ensure all nodes that could have heredocs nested know about their end lines.
43
+ - Ensure comments on assignment after the `=` before the value keep their place.
44
+ - Trailing comments on parameters with no parentheses now do not force a break.
45
+ - Allow `ArrayLiteral` opening brackets to have trailing comments.
46
+ - Allow different line suffix nodes to have different priorities.
47
+ - Better support for encoding by properly reading encoding magic comments.
48
+ - Support singleton single-line method definitions.
49
+ - Support `stree-ignore` comments to ignore formatting nodes.
50
+ - Add special formatting for arrays of `VarRef` nodes whose sum width is greater than 2 * the maximum width.
51
+ - Better output formatting for the CLI.
52
+
53
+ ### Changed
54
+
55
+ - Force a break if a block is attached to a `Command` or `CommandCall` node.
56
+ - Don't indent `CommandCall` arguments if they don't fit aligned.
57
+ - Force a break in `Call` nodes if there are comments on the receiver.
58
+ - Do not change block bounds if inside of a `Command` or `CommandCall` node.
59
+ - Handle empty parentheses inside method calls.
60
+ - Skip indentation for special array literals on assignment nodes.
61
+ - Ensure a final breakable is inserted when converting an `ArrayLiteral` to a `QSymbols`.
62
+ - Fix up the `doc_width` calculation for `CommandCall` nodes.
63
+ - Ensure parameters inside a lambda literal when there are no parentheses are grouped.
64
+ - Ensure when converting an `ArrayLiteral` to a `QWords` that the strings do not contain `[`.
65
+ - Stop looking for parent `Command` or `CommandCall` nodes in blocks once you hit `Statements`.
66
+ - Ensure nested `Lambda` nodes get their correct bounds.
67
+ - Ensure we do not change block bounds within control flow constructs.
68
+ - Ensure parentheses are added around keywords changing to their modifier forms.
69
+ - Allow conditionals to take modifier form if they are using the `then` keyword with a `VoidStmt`.
70
+ - `UntilMod` and `WhileMod` nodes that wrap a `Begin` should be forced into their modifier forms.
71
+ - Ensure `For` loops keep their trailing commas.
72
+ - Replicate content for `__END__` keyword exactly.
73
+ - Keep block `If`, `Unless`, `While`, and `Until` forms if there is an assignment in the predicate.
74
+ - Force using braces if the block is within the predicate of a conditional or loop.
75
+ - Allow for the possibility that `CommandCall` nodes might not have arguments.
76
+ - Explicitly handle `?"` so that it formats properly.
77
+ - Check that a block is within the predicate in a more relaxed way.
78
+ - Ensure the `Return` breaks with brackets and not parentheses.
79
+ - Ensure trailing comments on parameter declarations are consistent.
80
+ - Make `Command` and `CommandCall` aware that their arguments could exceed their normal expected bounds because of heredocs.
81
+ - Only unescape forward slashes in regular expressions if converting from slash bounds to `%r` bounds.
82
+ - Allow `When` nodes to grab trailing comments away from their statements lists.
83
+ - Allow flip-flop operators to be formatted correctly within `IfMod` and `UnlessMod` nodes.
84
+ - Allow `IfMod` and `UnlessMod` to know about heredocs moving their bounds.
85
+ - Properly handle breaking parameters when there are no parentheses.
86
+ - Properly handle trailing operators in call chains with attached comments.
87
+ - Force using braces if the block is within the predicate of a ternary.
88
+ - Properly handle trailing comments after a `then` operator on a `When` or `In` clause.
89
+ - Ensure nested `HshPtn` nodes use braces.
90
+ - Force using braces if the block is within a `Binary` within the predicate of a loop or conditional.
91
+ - Make sure `StringLiteral` and `StringEmbExpr` know that they can be extended by heredocs.
92
+ - Ensure `Int` nodes with preceding unary `+` get formatted properly.
93
+ - Properly handle byte-order mark column offsets at the beginnings of files.
94
+ - Ensure `Words`, `Symbols`, `QWords`, and `QSymbols` properly format when their contents contain brackets.
95
+ - Ensure ternaries being broken out into `if`...`else`...`end` get wrapped in parentheses if necessary.
96
+
97
+ ### Removed
98
+
99
+ - The `AccessCtrl` node in favor of just formatting correctly when you hit a `Statements` node.
100
+ - The `MethodAddArg` node is removed in favor of an optional `arguments` field on `Call` and `FCall`.
101
+
9
102
  ## [0.1.0] - 2021-11-16
10
103
 
11
104
  ### Added
12
105
 
13
106
  - 🎉 Initial release! 🎉
14
107
 
15
- [unreleased]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...HEAD
108
+ [unreleased]: https://github.com/kddnewton/syntax_tree/compare/v1.2.0...HEAD
109
+ [1.2.0]: https://github.com/kddnewton/syntax_tree/compare/v1.1.1...v1.2.0
110
+ [1.1.1]: https://github.com/kddnewton/syntax_tree/compare/v1.1.0...v1.1.1
111
+ [1.1.0]: https://github.com/kddnewton/syntax_tree/compare/v1.0.0...v1.1.0
112
+ [1.0.0]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...v1.0.0
16
113
  [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.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -9,8 +9,8 @@ GEM
9
9
  ast (2.4.2)
10
10
  benchmark-ips (2.9.2)
11
11
  docile (1.4.0)
12
- minitest (5.14.4)
13
- parser (3.0.3.1)
12
+ minitest (5.15.0)
13
+ parser (3.1.0.0)
14
14
  ast (~> 2.4.1)
15
15
  rake (13.0.6)
16
16
  ruby_parser (3.18.1)
@@ -26,6 +26,7 @@ GEM
26
26
 
27
27
  PLATFORMS
28
28
  x86_64-darwin-19
29
+ x86_64-darwin-21
29
30
  x86_64-linux
30
31
 
31
32
  DEPENDENCIES
@@ -40,4 +41,4 @@ DEPENDENCIES
40
41
  syntax_tree!
41
42
 
42
43
  BUNDLED WITH
43
- 2.2.15
44
+ 2.2.31
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Build Status](https://github.com/kddnewton/syntax_tree/workflows/Main/badge.svg)](https://github.com/kddnewton/syntax_tree/actions)
4
4
  [![Gem Version](https://img.shields.io/gem/v/syntax_tree.svg)](https://rubygems.org/gems/syntax_tree)
5
5
 
6
- A fast ripper subclass used for parsing and formatting Ruby code.
6
+ A fast Ruby parser and formatter with only standard library dependencies.
7
7
 
8
8
  ## Installation
9
9
 
@@ -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,244 @@
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 StandardError
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 StandardError
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
+ rescue StandardError
130
+ puts "\r#{filepath}"
131
+ raise
132
+ end
133
+ end
134
+
135
+ # The help message displayed if the input arguments are not correctly
136
+ # ordered or formatted.
137
+ HELP = <<~HELP
138
+ stree MODE FILE
139
+
140
+ MODE: ast | check | debug | doc | format | write
141
+ FILE: one or more paths to files to parse
142
+ HELP
143
+
144
+ class << self
145
+ # Run the CLI over the given array of strings that make up the arguments
146
+ # passed to the invocation.
147
+ def run(argv)
148
+ if argv.length < 2
149
+ warn(HELP)
150
+ return 1
151
+ end
152
+
153
+ arg, *patterns = argv
154
+ action =
155
+ case arg
156
+ when "a", "ast"
157
+ AST.new
158
+ when "c", "check"
159
+ Check.new
160
+ when "debug"
161
+ Debug.new
162
+ when "doc"
163
+ Doc.new
164
+ when "f", "format"
165
+ Format.new
166
+ when "w", "write"
167
+ Write.new
168
+ else
169
+ warn(HELP)
170
+ return 1
171
+ end
172
+
173
+ errored = false
174
+ patterns.each do |pattern|
175
+ Dir.glob(pattern).each do |filepath|
176
+ next unless File.file?(filepath)
177
+ source = SyntaxTree.read(filepath)
178
+
179
+ begin
180
+ action.run(filepath, source)
181
+ rescue ParseError => error
182
+ warn("Error: #{error.message}")
183
+
184
+ if error.lineno
185
+ highlight_error(error, source)
186
+ else
187
+ warn(error.message)
188
+ warn(error.backtrace)
189
+ end
190
+
191
+ errored = true
192
+ rescue Check::UnformattedError, Debug::NonIdempotentFormatError
193
+ errored = true
194
+ rescue => error
195
+ warn(error.message)
196
+ warn(error.backtrace)
197
+ errored = true
198
+ end
199
+ end
200
+ end
201
+
202
+ if errored
203
+ action.failure
204
+ 1
205
+ else
206
+ action.success
207
+ 0
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ # Highlights a snippet from a source and parse error.
214
+ def highlight_error(error, source)
215
+ lines = source.lines
216
+
217
+ maximum = [error.lineno + 3, lines.length].min
218
+ digits = Math.log10(maximum).ceil
219
+
220
+ ([error.lineno - 3, 0].max...maximum).each do |line_index|
221
+ line_number = line_index + 1
222
+
223
+ if line_number == error.lineno
224
+ part1 = Color.red(">")
225
+ part2 = Color.gray("%#{digits}d |" % line_number)
226
+ warn("#{part1} #{part2} #{colorize_line(lines[line_index])}")
227
+
228
+ part3 = Color.gray(" %#{digits}s |" % " ")
229
+ warn("#{part3} #{" " * error.column}#{Color.red("^")}")
230
+ else
231
+ prefix = Color.gray(" %#{digits}d |" % line_number)
232
+ warn("#{prefix} #{colorize_line(lines[line_index])}")
233
+ end
234
+ end
235
+ end
236
+
237
+ # Take a line of Ruby source and colorize the output.
238
+ def colorize_line(line)
239
+ require "irb"
240
+ IRB::Color.colorize_code(line, complete: false, ignore_error: true)
241
+ end
242
+ end
243
+ end
244
+ end
@@ -113,8 +113,10 @@ class PrettyPrint
113
113
  def pretty_print(q)
114
114
  q.text("breakable")
115
115
 
116
- attributes =
117
- [("force=true" if force?), ("indent=false" unless indent?)].compact
116
+ attributes = [
117
+ ("force=true" if force?),
118
+ ("indent=false" unless indent?)
119
+ ].compact
118
120
 
119
121
  if attributes.any?
120
122
  q.text("(")
@@ -213,9 +215,12 @@ class PrettyPrint
213
215
  # constantly check where the line ends to avoid accidentally printing some
214
216
  # content after a line suffix node.
215
217
  class LineSuffix
216
- attr_reader :contents
218
+ DEFAULT_PRIORITY = 1
217
219
 
218
- def initialize(contents: [])
220
+ attr_reader :priority, :contents
221
+
222
+ def initialize(priority: DEFAULT_PRIORITY, contents: [])
223
+ @priority = priority
219
224
  @contents = contents
220
225
  end
221
226
 
@@ -741,10 +746,17 @@ class PrettyPrint
741
746
 
742
747
  # This is a separate command stack that includes the same kind of triplets
743
748
  # 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.
749
+ # go at the end of printed lines once the other doc nodes are accounted for.
750
+ # Typically this is used to implement comments.
746
751
  line_suffixes = []
747
752
 
753
+ # This is a special sort used to order the line suffixes by both the
754
+ # priority set on the line suffix and the index it was in the original
755
+ # array.
756
+ line_suffix_sort = ->(line_suffix) do
757
+ [-line_suffix.last, -line_suffixes.index(line_suffix)]
758
+ end
759
+
748
760
  # This is a linear stack instead of a mutually recursive call defined on
749
761
  # the individual doc nodes for efficiency.
750
762
  while commands.any?
@@ -783,7 +795,7 @@ class PrettyPrint
783
795
  commands << [indent, mode, doc.flat_contents] if doc.flat_contents
784
796
  end
785
797
  when LineSuffix
786
- line_suffixes << [indent, mode, doc.contents]
798
+ line_suffixes << [indent, mode, doc.contents, doc.priority]
787
799
  when Breakable
788
800
  if mode == MODE_FLAT
789
801
  if doc.force?
@@ -804,7 +816,7 @@ class PrettyPrint
804
816
  # to flush them now, as we are about to add a newline.
805
817
  if line_suffixes.any?
806
818
  commands << [indent, mode, doc]
807
- commands += line_suffixes.reverse
819
+ commands += line_suffixes.sort_by(&line_suffix_sort)
808
820
  line_suffixes = []
809
821
  next
810
822
  end
@@ -838,7 +850,7 @@ class PrettyPrint
838
850
  end
839
851
 
840
852
  if commands.empty? && line_suffixes.any?
841
- commands += line_suffixes.reverse
853
+ commands += line_suffixes.sort_by(&line_suffix_sort)
842
854
  line_suffixes = []
843
855
  end
844
856
  end
@@ -1012,8 +1024,8 @@ class PrettyPrint
1012
1024
 
1013
1025
  # Inserts a LineSuffix node into the print tree. The contents of the node are
1014
1026
  # determined by the block.
1015
- def line_suffix
1016
- doc = LineSuffix.new
1027
+ def line_suffix(priority: LineSuffix::DEFAULT_PRIORITY)
1028
+ doc = LineSuffix.new(priority: priority)
1017
1029
  target << doc
1018
1030
 
1019
1031
  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.2.0"
7
7
  end