diff --git a/.gitignore b/.gitignore index 9106b2a3..76aa4f36 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /pkg/ /spec/reports/ /tmp/ + +test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 874edaf8..e637bb64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] -## [0.1.0] - 2020-10-09 +## [0.1.0] - 2021-11-16 ### Added - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/kddeisz/template/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/kddeisz/template/compare/39c5a7...v0.1.0 +[unreleased]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/kddnewton/syntax_tree/compare/8aa1f5...v0.1.0 diff --git a/Gemfile b/Gemfile index 7f4f5e95..7b6f1d74 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,10 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" gemspec + +gem "benchmark-ips" +gem "parser" +gem "ruby_parser" +gem "stackprof" diff --git a/Gemfile.lock b/Gemfile.lock index 1d06e0c7..94bd8c7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,30 +1,43 @@ PATH remote: . specs: - template (0.1.0) + syntax_tree (0.1.0) GEM remote: https://rubygems.org/ specs: - docile (1.3.5) + ast (2.4.2) + benchmark-ips (2.9.2) + docile (1.4.0) minitest (5.14.4) + parser (3.0.3.1) + ast (~> 2.4.1) rake (13.0.6) + ruby_parser (3.18.1) + sexp_processor (~> 4.16) + sexp_processor (4.16.0) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) - simplecov_json_formatter (0.1.2) + simplecov_json_formatter (0.1.3) + stackprof (0.2.17) PLATFORMS - ruby + x86_64-darwin-19 + x86_64-linux DEPENDENCIES + benchmark-ips bundler minitest + parser rake + ruby_parser simplecov - template! + stackprof + syntax_tree! BUNDLED WITH - 2.2.3 + 2.2.15 diff --git a/README.md b/README.md index 1557c03c..6550c1b0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -# Template +# SyntaxTree -[![Build Status](https://github.com/kddnewton/ripper-parse_tree/workflows/Main/badge.svg)](https://github.com/kddnewton/ripper-parse_tree/actions) -[![Gem Version](https://img.shields.io/gem/v/ripper-parse_tree.svg)](https://rubygems.org/gems/ripper-parse_tree) +[![Build Status](https://github.com/kddnewton/syntax_tree/workflows/Main/badge.svg)](https://github.com/kddnewton/syntax_tree/actions) +[![Gem Version](https://img.shields.io/gem/v/syntax_tree.svg)](https://rubygems.org/gems/syntax_tree) + +A fast ripper subclass used for parsing and formatting Ruby code. ## Installation Add this line to your application's Gemfile: ```ruby -gem 'ripper-parse_tree' +gem "syntax_tree" ``` And then execute: @@ -17,10 +19,36 @@ And then execute: Or install it yourself as: - $ gem install ripper-parse_tree + $ gem install syntax_tree ## Usage +From code: + +```ruby +require "syntax_tree" + +pp SyntaxTree.parse(source) # print out the AST +puts SyntaxTree.format(source) # format the AST +``` + +From the CLI: + +```sh +$ stree ast program.rb +(program + (statements + ... +``` + +or + +```sh +$ stree format program.rb +class MyClass + ... +``` + ## Development 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. @@ -29,7 +57,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/kddeisz/ripper-parse_tree. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/kddnewton/ripper-parse_tree/blob/main/CODE_OF_CONDUCT.md). +Bug reports and pull requests are welcome on GitHub at https://github.com/kddnewton/syntax_tree. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/kddnewton/syntax_tree/blob/main/CODE_OF_CONDUCT.md). ## License @@ -37,4 +65,4 @@ The gem is available as open source under the terms of the [MIT License](https:/ ## Code of Conduct -Everyone interacting in the Template project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kddeisz/ripper-parse_tree/blob/main/CODE_OF_CONDUCT.md). +Everyone interacting in the syntax_tree project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kddnewton/syntax_tree/blob/main/CODE_OF_CONDUCT.md). diff --git a/bin/bench b/bin/bench new file mode 100755 index 00000000..307d3044 --- /dev/null +++ b/bin/bench @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'benchmark/ips' + +require_relative '../lib/syntax_tree' +require 'ruby_parser' +require 'parser/current' + +def compare(filepath) + prefix = "#{File.expand_path('..', __dir__)}/" + puts "=== #{filepath.delete_prefix(prefix)} ===" + + source = File.read(filepath) + + Benchmark.ips do |x| + x.report('syntax_tree') { SyntaxTree.new(source).parse } + x.report('parser') { Parser::CurrentRuby.parse(source) } + x.report('ruby_parser') { RubyParser.new.parse(source) } + x.compare! + end +end + +filepaths = ARGV + +# If the user didn't supply any files to parse to benchmark, then we're going to +# default to parsing this file and the main syntax_tree file (a small and large +# file). +if filepaths.empty? + filepaths = [ + File.expand_path('bench', __dir__), + File.expand_path('../lib/syntax_tree.rb', __dir__) + ] +end + +filepaths.each { |filepath| compare(filepath) } diff --git a/bin/console b/bin/console index eb8d8536..e19966f5 100755 --- a/bin/console +++ b/bin/console @@ -2,7 +2,7 @@ # frozen_string_literal: true require 'bundler/setup' -require 'ripper/parse_tree' +require 'syntax_tree' require 'irb' IRB.start(__FILE__) diff --git a/bin/profile b/bin/profile new file mode 100755 index 00000000..86c025e4 --- /dev/null +++ b/bin/profile @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'stackprof' + +filepath = File.expand_path('../lib/syntax_tree', __dir__) +require_relative filepath + +GC.disable + +StackProf.run(mode: :cpu, out: "tmp/profile.dump", raw: true) do + SyntaxTree.format(File.read("#{filepath}.rb")) +end + +GC.enable + +`bundle exec stackprof --d3-flamegraph tmp/profile.dump > tmp/flamegraph.html` +puts "open tmp/flamegraph.html" diff --git a/exe/stree b/exe/stree new file mode 100755 index 00000000..05b09494 --- /dev/null +++ b/exe/stree @@ -0,0 +1,84 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative File.expand_path("../lib/syntax_tree", __dir__) + +help = <<~EOF + stree MDOE FILE + + MODE: one of "a", "ast", "d", "doc", "f", "format", "w", or "write" + FILE: one or more paths to files to parse +EOF + +if ARGV.length < 2 + warn(help) + exit(1) +end + +module SyntaxTree::CLI + class AST + def run(filepath) + pp SyntaxTree.parse(File.read(filepath)) + end + end + + class Doc + def run(filepath) + formatter = SyntaxTree::Formatter.new([]) + SyntaxTree.parse(File.read(filepath)).format(formatter) + pp formatter.groups.first + end + end + + class Format + def run(filepath) + puts SyntaxTree.format(File.read(filepath)) + end + end + + class Write + def run(filepath) + File.write(filepath, SyntaxTree.format(File.read(filepath))) + end + end +end + +mode = + case ARGV.shift + when "a", "ast" + SyntaxTree::CLI::AST.new + when "d", "doc" + SyntaxTree::CLI::Doc.new + when "f", "format" + SyntaxTree::CLI::Format.new + when "w", "write" + SyntaxTree::CLI::Write.new + else + warn(help) + exit(1) + end + +queue = Queue.new +ARGV.each { |pattern| Dir[pattern].each { |filepath| queue << filepath } } + +if queue.size <= 1 + filepath = queue.shift + mode.run(filepath) if File.file?(filepath) + return +end + +count = [8, queue.size].min +threads = + count.times.map do + Thread.new do + loop do + filepath = queue.shift + break if filepath == :exit + + mode.run(filepath) if File.file?(filepath) + end + end + end + +count.times { queue << :exit } +threads.each(&:join) diff --git a/lib/ripper/parse_tree/version.rb b/lib/ripper/parse_tree/version.rb deleted file mode 100644 index 300e6156..00000000 --- a/lib/ripper/parse_tree/version.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class Ripper - class ParseTree - VERSION = '0.1.0' - end -end diff --git a/lib/ripper/parse_tree.rb b/lib/syntax_tree.rb similarity index 57% rename from lib/ripper/parse_tree.rb rename to lib/syntax_tree.rb index 7bebf9dc..9c90b0d5 100644 --- a/lib/ripper/parse_tree.rb +++ b/lib/syntax_tree.rb @@ -1,8 +1,27 @@ # frozen_string_literal: true -require 'ripper' +require "pp" +require "prettyprint" +require "ripper" +require "stringio" -class Ripper::ParseTree < Ripper +require_relative "syntax_tree/version" + +# If PrettyPrint::Assign isn't defined, then we haven't gotten the updated +# version of prettyprint. In that case we'll define our own. This is going to +# overwrite a bunch of methods, so silencing them as well. +unless PrettyPrint.const_defined?(:Align) + verbose = $VERBOSE + $VERBOSE = nil + + begin + require_relative "syntax_tree/prettyprint" + ensure + $VERBOSE = verbose + end +end + +class SyntaxTree < Ripper # Represents a line in the source. If this class is being used, it means that # every character in the string is 1 byte in length, so we can just return the # start of the line + the index. @@ -24,11 +43,9 @@ class MultiByteString def initialize(start, line) @indices = [] - line - .each_char - .with_index(start) do |char, index| - char.bytesize.times { @indices << index } - end + line.each_char.with_index(start) do |char, index| + char.bytesize.times { @indices << index } + end end def [](byteindex) @@ -47,6 +64,13 @@ def initialize(start_line:, start_char:, end_line:, end_char:) @end_char = end_char end + def ==(other) + other.is_a?(Location) && start_line == other.start_line && + start_char == other.start_char && + end_line == other.end_line && + end_char == other.end_char + end + def to(other) Location.new( start_line: start_line, @@ -86,14 +110,80 @@ def initialize(error, lineno, column) end end - attr_reader :source, :lines, :tokens + # A slightly enhanced PP that knows how to format recursively including + # comments. + class Formatter < PP + attr_reader :stack, :quote + + def initialize(*) + super + @stack = [] + @quote = "\"" + end + + def format(node) + stack << node + doc = nil + + # If there are comments, then we're going to format them around the node + # so that they get printed properly. + if node.comments.any? + leading, trailing = node.comments.partition(&:leading?) + + # Print all comments that were found before the node. + leading.each do |comment| + comment.format(self) + breakable(force: true) + end + + doc = node.format(self) + + # Print all comments that were found after the node. + trailing.each do |comment| + line_suffix do + text(" ") + comment.format(self) + break_parent + end + end + else + doc = node.format(self) + end + + stack.pop + doc + end + + def format_each(nodes) + nodes.each { |node| format(node) } + end + + def parent + stack[-2] + end + + def parents + stack[0...-1].reverse_each + end + end + + # [String] the source being parsed + attr_reader :source + + # [Array[ String ]] the list of lines in the source + attr_reader :lines - # This is an attr_accessor so Stmts objects can grab comments out of this - # array and attach them to themselves. - attr_accessor :comments + # [Array[ untyped ]] a running list of tokens that have been found in the + # source. This list changes a lot as certain nodes will "consume" these tokens + # to determine their bounds. + attr_reader :tokens - def initialize(source, *args) - super(source, *args) + # [Array[ Comment | EmbDoc ]] the list of comments that have been found while + # parsing the source. + attr_reader :comments + + def initialize(source, *) + super # We keep the source around so that we can refer back to it when we're # generating the AST. Sometimes it's easier to just reference the source @@ -105,7 +195,7 @@ def initialize(source, *args) # check if certain lines contain certain characters. For example, we'll use # this to generate the content that goes after the __END__ keyword. Or we'll # use this to check if a comment has other content on its line. - @lines = source.split("\n") + @lines = source.split(/\r?\n/) # This is the full set of comments that have been found by the parser. It's # a running list. At the end of every block of statements, they will go in @@ -161,10 +251,19 @@ def initialize(source, *args) end def self.parse(source) - builder = new(source) + parser = new(source) + response = parser.parse + response unless parser.error? + end + + def self.format(source) + output = [] + + formatter = Formatter.new(output) + parse(source).format(formatter) - response = builder.parse - response unless builder.error? + formatter.flush + output.join end private @@ -225,7 +324,7 @@ def find_token(type, value = :any, consume: true) def find_colon2_before(const) index = tokens.rindex do |token| - token.is_a?(Op) && token.value == '::' && + token.is_a?(Op) && token.value == "::" && token.location.start_char < const.location.start_char end @@ -246,7 +345,7 @@ def find_colon2_before(const) def find_next_statement_start(position) remaining = source[position..-1] - if remaining.sub(/\A +/, '')[0] == '#' + if remaining.sub(/\A +/, "")[0] == "#" return position + remaining.index("\n") end @@ -277,17 +376,41 @@ class BEGINBlock # [Location] the location of this node attr_reader :location - def initialize(lbrace:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, statements:, location:, comments: []) @lbrace = lbrace @statements = statements @location = location + @comments = comments + end + + def child_nodes + [lbrace, statements] + end + + def format(q) + q.group do + q.text("BEGIN ") + q.format(lbrace) + q.indent do + q.breakable + q.format(statements) + end + q.breakable + q.text("}") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('BEGIN') + q.group(2, "(", ")") do + q.text("BEGIN") + q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -296,7 +419,8 @@ def to_json(*opts) type: :BEGIN, lbrace: lbrace, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -312,7 +436,7 @@ def on_BEGIN(statements) rbrace.location.start_char ) - keyword = find_token(Kw, 'BEGIN') + keyword = find_token(Kw, "BEGIN") BEGINBlock.new( lbrace: lbrace, @@ -334,35 +458,54 @@ class CHAR # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + if value.length != 2 + q.text(value) + else + q.text(q.quote) + q.text(value[1]) + q.text(q.quote) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('CHAR') + q.group(2, "(", ")") do + q.text("CHAR") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@CHAR, value: value, loc: location }.to_json(*opts) + { type: :CHAR, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_CHAR: (String value) -> CHAR def on_CHAR(value) - node = - CHAR.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + CHAR.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # ENDBlock represents the use of the +END+ keyword, which hooks into the @@ -384,24 +527,52 @@ class ENDBlock # [Location] the location of this node attr_reader :location - def initialize(lbrace:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, statements:, location:, comments: []) @lbrace = lbrace @statements = statements @location = location + @comments = comments + end + + def child_nodes + [lbrace, statements] + end + + def format(q) + q.group do + q.text("END ") + q.format(lbrace) + q.indent do + q.breakable + q.format(statements) + end + q.breakable + q.text("}") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('END') + q.group(2, "(", ")") do + q.text("END") + q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :END, lbrace: lbrace, stmts: statements, loc: location }.to_json( - *opts - ) + { + type: :END, + lbrace: lbrace, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -416,7 +587,7 @@ def on_END(statements) rbrace.location.start_char ) - keyword = find_token(Kw, 'END') + keyword = find_token(Kw, "END") ENDBlock.new( lbrace: lbrace, @@ -441,21 +612,40 @@ class EndContent # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text("__END__") + q.breakable(force: true) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('__end__') + q.group(2, "(", ")") do + q.text("__end__") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@__end__, value: value, loc: location }.to_json(*opts) + { type: :__end__, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -481,6 +671,31 @@ def on___end__(value) # symbols (note that this includes dynamic symbols like # :"left-#{middle}-right"). class Alias + class AliasArgumentFormatter + # [DynaSymbol | SymbolLiteral] the argument being passed to alias + attr_reader :argument + + def initialize(argument) + @argument = argument + end + + def comments + if argument.is_a?(SymbolLiteral) + argument.comments + argument.value.comments + else + argument.comments + end + end + + def format(q) + if argument.is_a?(SymbolLiteral) + q.format(argument.value) + else + q.format(argument) + end + end + end + # [DynaSymbol | SymbolLiteral] the new name of the method attr_reader :left @@ -490,24 +705,58 @@ class Alias # [Location] the location of this node attr_reader :location - def initialize(left:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) @left = left @right = right @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + def format(q) + keyword = "alias " + left_argument = AliasArgumentFormatter.new(left) + + q.group do + q.text(keyword) + q.format(left_argument) + q.group do + q.nest(keyword.length) do + q.breakable(force: left_argument.comments.any?) + q.format(AliasArgumentFormatter.new(right)) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('alias') + q.group(2, "(", ")") do + q.text("alias") + q.breakable q.pp(left) + q.breakable q.pp(right) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :alias, left: left, right: right, loc: location }.to_json(*opts) + { + type: :alias, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -517,7 +766,7 @@ def to_json(*opts) # (DynaSymbol | SymbolLiteral) right # ) -> Alias def on_alias(left, right) - keyword = find_token(Kw, 'alias') + keyword = find_token(Kw, "alias") Alias.new( left: left, @@ -543,25 +792,54 @@ class ARef # [untyped] the value being indexed attr_reader :collection - # [nil | Args | ArgsAddBlock] the value being passed within the brackets + # [nil | Args] the value being passed within the brackets attr_reader :index # [Location] the location of this node attr_reader :location - def initialize(collection:, index:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(collection:, index:, location:, comments: []) @collection = collection @index = index @location = location + @comments = comments + end + + def child_nodes + [collection, index] + end + + def format(q) + q.group do + q.format(collection) + q.text("[") + + if index + q.indent do + q.breakable("") + q.format(index) + end + q.breakable("") + end + + q.text("]") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('aref') + q.group(2, "(", ")") do + q.text("aref") + q.breakable q.pp(collection) + q.breakable q.pp(index) + + q.pp(Comment::List.new(comments)) end end @@ -570,13 +848,14 @@ def to_json(*opts) type: :aref, collection: collection, index: index, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end # :call-seq: - # on_aref: (untyped collection, (nil | Args | ArgsAddBlock) index) -> ARef + # on_aref: (untyped collection, (nil | Args) index) -> ARef def on_aref(collection, index) find_token(LBracket) rbracket = find_token(RBracket) @@ -599,25 +878,54 @@ class ARefField # [untyped] the value being indexed attr_reader :collection - # [nil | ArgsAddBlock] the value being passed within the brackets + # [nil | Args] the value being passed within the brackets attr_reader :index # [Location] the location of this node attr_reader :location - def initialize(collection:, index:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(collection:, index:, location:, comments: []) @collection = collection @index = index @location = location + @comments = comments + end + + def child_nodes + [collection, index] + end + + def format(q) + q.group do + q.format(collection) + q.text("[") + + if index + q.indent do + q.breakable("") + q.format(index) + end + q.breakable("") + end + + q.text("]") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('aref_field') + q.group(2, "(", ")") do + q.text("aref_field") + q.breakable q.pp(collection) + q.breakable q.pp(index) + + q.pp(Comment::List.new(comments)) end end @@ -626,7 +934,8 @@ def to_json(*opts) type: :aref_field, collection: collection, index: index, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -634,7 +943,7 @@ def to_json(*opts) # :call-seq: # on_aref_field: ( # untyped collection, - # (nil | ArgsAddBlock) index + # (nil | Args) index # ) -> ARefField def on_aref_field(collection, index) find_token(LBracket) @@ -656,42 +965,72 @@ def on_aref_field(collection, index) # # method(argument) # - # In the example above, there would be an ArgParen node around the - # ArgsAddBlock node that represents the set of arguments being sent to the - # method method. The argument child node can be +nil+ if no arguments were - # passed, as in: + # In the example above, there would be an ArgParen node around the Args node + # that represents the set of arguments being sent to the method method. The + # argument child node can be +nil+ if no arguments were passed, as in: # # method() # class ArgParen - # [nil | Args | ArgsAddBlock | ArgsForward] the arguments inside the + # [nil | Args | ArgsForward] the arguments inside the # parentheses attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + def format(q) + unless arguments + q.text("()") + return + end + + q.group(0, "(", ")") do + q.indent do + q.breakable("") + q.format(arguments) + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('arg_paren') + q.group(2, "(", ")") do + q.text("arg_paren") + q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :arg_paren, args: arguments, loc: location }.to_json(*opts) + { + type: :arg_paren, + args: arguments, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_arg_paren: ( - # (nil | Args | ArgsAddBlock | ArgsForward) arguments + # (nil | Args | ArgsForward) arguments # ) -> ArgParen def on_arg_paren(arguments) lparen = find_token(LParen) @@ -719,41 +1058,51 @@ def on_arg_paren(arguments) # method(first, second, third) # class Args - # Array[untyped] the arguments that this node wraps + # [Array[ untyped ]] the arguments that this node wraps attr_reader :parts # [Location] the location of this node attr_reader :location - def initialize(parts:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) @parts = parts @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + q.seplist(parts) { |part| q.format(part) } end def pretty_print(q) - q.group(2, '(', ')') do - q.text('args') + q.group(2, "(", ")") do + q.text("args") + q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :args, parts: parts, loc: location }.to_json(*opts) + { type: :args, parts: parts, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_args_add: ((Args | ArgsAddStar) arguments, untyped argument) -> Args + # on_args_add: (Args arguments, untyped argument) -> Args def on_args_add(arguments, argument) - if arguments.is_a?(ArgsAddStar) - # If we're adding an argument after a splatted argument, then it's going - # to come in through this path. - Args.new( - parts: [arguments, argument], - location: arguments.location.to(argument.location) - ) - elsif arguments.parts.empty? + if arguments.parts.empty? # If this is the first argument being passed into the list of arguments, # then we're going to use the bounds of the argument to override the # parent node's location since this will be more accurate. @@ -768,119 +1117,141 @@ def on_args_add(arguments, argument) end end - # ArgsAddBlock represents a list of arguments and potentially a block - # argument. ArgsAddBlock is commonly seen being passed to any method where you - # use parentheses (wrapped in an ArgParen node). It’s also used to pass - # arguments to the various control-flow keywords like +return+. + # ArgBlock represents using a block operator on an expression. # - # method(argument, &block) + # method(&expression) # - class ArgsAddBlock - # [Args | ArgsAddStar] the arguments before the optional block - attr_reader :arguments - - # [nil | untyped] the optional block argument - attr_reader :block + class ArgBlock + # [untyped] the expression being turned into a block + attr_reader :value # [Location] the location of this node attr_reader :location - def initialize(arguments:, block:, location:) - @arguments = arguments - @block = block + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.text("&") + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('args_add_block') - q.breakable - q.pp(arguments) + q.group(2, "(", ")") do + q.text("arg_block") + q.breakable - q.pp(block) + q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { - type: :args_add_block, - args: arguments, - block: block, - loc: location - }.to_json(*opts) + { type: :arg_block, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_args_add_block: ( - # (Args | ArgsAddStar) arguments, + # Args arguments, # (false | untyped) block - # ) -> ArgsAddBlock + # ) -> Args def on_args_add_block(arguments, block) - ending = block || arguments + return arguments unless block - ArgsAddBlock.new( - arguments: arguments, - block: block || nil, - location: arguments.location.to(ending.location) + arg_block = + ArgBlock.new( + value: block, + location: find_token(Op, "&").location.to(block.location) + ) + + Args.new( + parts: arguments.parts << arg_block, + location: arguments.location.to(arg_block.location) ) end - # ArgsAddStar represents adding a splat of values to a list of arguments. + # Star represents using a splat operator on an expression. # - # method(prefix, *arguments, suffix) + # method(*arguments) # - class ArgsAddStar - # [Args | ArgsAddStar] the arguments before the starred argument - attr_reader :arguments - - # [untyped] the expression being starred - attr_reader :star + class ArgStar + # [nil | untyped] the expression being splatted + attr_reader :value # [Location] the location of this node attr_reader :location - def initialize(arguments:, star:, location:) - @arguments = arguments - @star = star + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.text("*") + q.format(value) if value end def pretty_print(q) - q.group(2, '(', ')') do - q.text('args_add_star') - q.breakable - q.pp(arguments) + q.group(2, "(", ")") do + q.text("arg_star") + q.breakable - q.pp(star) + q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { - type: :args_add_star, - args: arguments, - star: star, - loc: location - }.to_json(*opts) + { type: :arg_star, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_args_add_star: ( - # (Args | ArgsAddStar) arguments, - # untyped star - # ) -> ArgsAddStar - def on_args_add_star(arguments, star) - beginning = find_token(Op, '*') - ending = star || beginning + # on_args_add_star: (Args arguments, untyped star) -> Args + def on_args_add_star(arguments, argument) + beginning = find_token(Op, "*") + ending = argument || beginning - ArgsAddStar.new( - arguments: arguments, - star: star, - location: beginning.location.to(ending.location) - ) - end + location = + if arguments.parts.empty? + ending.location + else + arguments.location.to(ending.location) + end + + arg_star = + ArgStar.new( + value: argument, + location: beginning.location.to(ending.location) + ) + + Args.new(parts: arguments.parts << arg_star, location: location) + end # ArgsForward represents forwarding all kinds of arguments onto another method # call. @@ -906,28 +1277,48 @@ class ArgsForward # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('args_forward') + q.group(2, "(", ")") do + q.text("args_forward") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :args_forward, value: value, loc: location }.to_json(*opts) + { + type: :args_forward, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_args_forward: () -> ArgsForward def on_args_forward - op = find_token(Op, '...') + op = find_token(Op, "...") ArgsForward.new(value: op.value, location: op.location) end @@ -938,53 +1329,140 @@ def on_args_new Args.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) end - # ArrayLiteral represents any form of an array literal, and contains myriad - # child nodes because of the special array literal syntax like %w and %i. + # ArrayLiteral represents an array literal, which can optionally contain + # elements. # # [] # [one, two, three] - # [*one_two_three] - # %i[one two three] - # %w[one two three] - # %I[one two three] - # %W[one two three] # - # Every line in the example above produces an ArrayLiteral node. In order, the - # child contents node of this ArrayLiteral node would be nil, Args, - # ArgsAddStar, QSymbols, QWords, Symbols, and Words. class ArrayLiteral - # [nil | Args | ArgsAddStar | Qsymbols | Qwords | Symbols | Words] the - # contents of the array + class QWordsFormatter + # [Args] the contents of the array + attr_reader :contents + + def initialize(contents) + @contents = contents + end + + def format(q) + q.group(0, "%w[", "]") do + q.indent do + q.breakable("") + q.seplist(contents.parts, -> { q.breakable }) do |part| + q.format(part.parts.first) + end + end + q.breakable("") + end + end + end + + class QSymbolsFormatter + # [Args] the contents of the array + attr_reader :contents + + def initialize(contents) + @contents = contents + end + + def format(q) + q.group(0, "%i[", "]") do + q.indent do + q.breakable("") + q.seplist(contents.parts, -> { q.breakable }) do |part| + q.format(part.value) + end + end + end + end + end + + # [nil | Args] the contents of the array attr_reader :contents # [Location] the location of this node attr_reader :location - def initialize(contents:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(contents:, location:, comments: []) @contents = contents @location = location + @comments = comments + end + + def child_nodes + [contents] + end + + def format(q) + unless contents + q.text("[]") + return + end + + if qwords? + QWordsFormatter.new(contents).format(q) + return + end + + if qsymbols? + QSymbolsFormatter.new(contents).format(q) + return + end + + q.group(0, "[", "]") do + q.indent do + q.breakable("") + q.format(contents) + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('array') + q.group(2, "(", ")") do + q.text("array") + q.breakable q.pp(contents) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :array, cnts: contents, loc: location }.to_json(*opts) + { type: :array, cnts: contents, loc: location, cmts: comments }.to_json( + *opts + ) + end + + private + + def qwords? + contents && contents.comments.empty? && contents.parts.length > 1 && + contents.parts.all? do |part| + part.is_a?(StringLiteral) && part.comments.empty? && + part.parts.length == 1 && + part.parts.first.is_a?(TStringContent) && + !part.parts.first.value.match?(/[\s\\\]]/) + end + end + + def qsymbols? + contents && contents.comments.empty? && contents.parts.length > 1 && + contents.parts.all? do |part| + part.is_a?(SymbolLiteral) && part.comments.empty? + end end end # :call-seq: - # on_array: ( - # (nil | Args | ArgsAddStar | Qsymbols | Qwords | Symbols | Words) - # contents - # ) -> ArrayLiteral + # on_array: ((nil | Args) contents) -> + # ArrayLiteral | QSymbols | QWords | Symbols | Words def on_array(contents) - if !contents || contents.is_a?(Args) || contents.is_a?(ArgsAddStar) + if !contents || contents.is_a?(Args) lbracket = find_token(LBracket) rbracket = find_token(RBracket) @@ -994,13 +1472,11 @@ def on_array(contents) ) else tstring_end = find_token(TStringEnd) - contents = - contents.class.new( - elements: contents.elements, - location: contents.location.to(tstring_end.location) - ) - ArrayLiteral.new(contents: contents, location: contents.location) + contents.class.new( + elements: contents.elements, + location: contents.location.to(tstring_end.location) + ) end end @@ -1022,35 +1498,91 @@ def on_array(contents) # and an optional array of positional matches that occur after the splat. # All of the in clauses above would create an AryPtn node. class AryPtn + class RestFormatter + # [VarField] the identifier that represents the remaining positionals + attr_reader :value + + def initialize(value) + @value = value + end + + def comments + value.comments + end + + def format(q) + q.text("*") + q.format(value) + end + end + # [nil | VarRef] the optional constant wrapper attr_reader :constant - # [Array[untyped]] the regular positional arguments that this array pattern - # is matching against + # [Array[ untyped ]] the regular positional arguments that this array + # pattern is matching against attr_reader :requireds # [nil | VarField] the optional starred identifier that grabs up a list of # positional arguments attr_reader :rest - # [Array[untyped]] the list of positional arguments occurring after the + # [Array[ untyped ]] the list of positional arguments occurring after the # optional star if there is one attr_reader :posts # [Location] the location of this node attr_reader :location - def initialize(constant:, requireds:, rest:, posts:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + constant:, + requireds:, + rest:, + posts:, + location:, + comments: [] + ) @constant = constant @requireds = requireds @rest = rest @posts = posts @location = location + @comments = comments + end + + def child_nodes + [constant, *required, rest, *posts] + end + + def format(q) + parts = [*requireds] + parts << RestFormatter.new(rest) if rest + parts += posts + + if constant + q.format(constant) + q.text("[") + q.seplist(parts) { |part| q.format(part) } + q.text("]") + return + end + + parent = q.parent + if parts.length == 1 || PATTERNS.any? { |pattern| parent.is_a?(pattern) } + q.text("[") + q.seplist(parts) { |part| q.format(part) } + q.text("]") + else + q.seplist(parts) { |part| q.format(part) } + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('aryptn') + q.group(2, "(", ")") do + q.text("aryptn") if constant q.breakable @@ -1059,7 +1591,7 @@ def pretty_print(q) if requireds.any? q.breakable - q.group(2, '(', ')') do + q.group(2, "(", ")") do q.seplist(requireds) { |required| q.pp(required) } end end @@ -1071,8 +1603,10 @@ def pretty_print(q) if posts.any? q.breakable - q.group(2, '(', ')') { q.seplist(posts) { |post| q.pp(post) } } + q.group(2, "(", ")") { q.seplist(posts) { |post| q.pp(post) } } end + + q.pp(Comment::List.new(comments)) end end @@ -1083,7 +1617,8 @@ def to_json(*opts) reqs: requireds, rest: rest, posts: posts, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -1114,7 +1649,7 @@ def on_aryptn(constant, requireds, rest, posts) # variable = value # class Assign - # [ArefField | ConstPathField | Field | TopConstField | VarField] the target + # [ARefField | ConstPathField | Field | TopConstField | VarField] the target # to assign the result of the expression to attr_reader :target @@ -1124,32 +1659,74 @@ class Assign # [Location] the location of this node attr_reader :location - def initialize(target:, value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, value:, location:, comments: []) @target = target @value = value @location = location + @comments = comments + end + + def child_nodes + [target, value] + end + + def format(q) + q.group do + q.format(target) + q.text(" =") + + if skip_indent? + q.text(" ") + q.format(value) + else + q.indent do + q.breakable + q.format(value) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('assign') + q.group(2, "(", ")") do + q.text("assign") + q.breakable q.pp(target) + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :assign, target: target, value: value, loc: location }.to_json( - *opts - ) + { + type: :assign, + target: target, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + def skip_indent? + target.is_a?(ARefField) || value.is_a?(ArrayLiteral) || + value.is_a?(HashLiteral) || + value.is_a?(Heredoc) || + value.is_a?(Lambda) end end # :call-seq: # on_assign: ( - # (ArefField | ConstPathField | Field | TopConstField | VarField) target, + # (ARefField | ConstPathField | Field | TopConstField | VarField) target, # untyped value # ) -> Assign def on_assign(target, value) @@ -1160,13 +1737,13 @@ def on_assign(target, value) ) end - # AssocNew represents a key-value pair within a hash. It is a child node of + # Assoc represents a key-value pair within a hash. It is a child node of # either an AssocListFromArgs or a BareAssocHash. # # { key1: value1, key2: value2 } # # In the above example, the would be two AssocNew nodes. - class AssocNew + class Assoc # [untyped] the key of this pair attr_reader :key @@ -1176,35 +1753,61 @@ class AssocNew # [Location] the location of this node attr_reader :location - def initialize(key:, value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(key:, value:, location:, comments: []) @key = key @value = value @location = location + @comments = comments + end + + def child_nodes + [key, value] + end + + def format(q) + contents = -> do + q.parent.format_key(q, key) + q.indent do + q.breakable + q.format(value) + end + end + + value.is_a?(HashLiteral) ? contents.call : q.group(&contents) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('assoc_new') + q.group(2, "(", ")") do + q.text("assoc") + q.breakable q.pp(key) + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :assoc_new, key: key, value: value, loc: location }.to_json(*opts) + { + type: :assoc, + key: key, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: - # on_assoc_new: (untyped key, untyped value) -> AssocNew + # on_assoc_new: (untyped key, untyped value) -> Assoc def on_assoc_new(key, value) - AssocNew.new( - key: key, - value: value, - location: key.location.to(value.location) - ) + Assoc.new(key: key, value: value, location: key.location.to(value.location)) end # AssocSplat represents double-splatting a value into a hash (either a hash @@ -1219,74 +1822,56 @@ class AssocSplat # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.text("**") + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('assoc_splat') + q.group(2, "(", ")") do + q.text("assoc_splat") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :assoc_splat, value: value, loc: location }.to_json(*opts) + { + type: :assoc_splat, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_assoc_splat: (untyped value) -> AssocSplat def on_assoc_splat(value) - operator = find_token(Op, '**') + operator = find_token(Op, "**") AssocSplat.new(value: value, location: operator.location.to(value.location)) end - # AssocListFromArgs represents the key-value pairs of a hash literal. Its - # parent node is always a hash. - # - # { key1: value1, key2: value2 } - # - class AssocListFromArgs - # [Array[AssocNew | AssocSplat]] - attr_reader :assocs - - # [Location] the location of this node - attr_reader :location - - def initialize(assocs:, location:) - @assocs = assocs - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('assoclist_from_args') - q.breakable - q.group(2, '(', ')') { q.seplist(assocs) { |assoc| q.pp(assoc) } } - end - end - - def to_json(*opts) - { type: :assoclist_from_args, assocs: assocs, loc: location }.to_json( - *opts - ) - end - end - - # :call-seq: - # on_assoclist_from_args: ( - # Array[AssocNew | AssocSplat] assocs - # ) -> AssocListFromArgs - def on_assoclist_from_args(assocs) - AssocListFromArgs.new( - assocs: assocs, - location: assocs[0].location.to(assocs[-1].location) - ) - end + # def on_assoclist_from_args(assocs) + # assocs + # end # Backref represents a global variable referencing a matched value. It comes # in the form of a $ followed by a positive integer. @@ -1300,35 +1885,48 @@ class Backref # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('backref') + q.group(2, "(", ")") do + q.text("backref") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@backref, value: value, loc: location }.to_json(*opts) + { type: :backref, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_backref: (String value) -> Backref def on_backref(value) - node = - Backref.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + Backref.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # Backtick represents the use of the ` operator. It's usually found being used @@ -1341,21 +1939,38 @@ class Backtick # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('backtick') + q.group(2, "(", ")") do + q.text("backtick") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@backtick, value: value, loc: location }.to_json(*opts) + { type: :backtick, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -1372,6 +1987,86 @@ def on_backtick(value) node end + # This module is responsible for formatting the assocs contained within a + # hash or bare hash. It first determines if every key in the hash can use + # labels. If it can, it uses labels. Otherwise it uses hash rockets. + module HashFormatter + class Base + # [HashLiteral | BareAssocHash] the source of the assocs + attr_reader :container + + def initialize(container) + @container = container + end + + def comments + container.comments + end + + def format(q) + q.seplist(container.assocs) { |assoc| q.format(assoc) } + end + end + + class Labels < Base + def format_key(q, key) + case key + when Label + q.format(key) + when SymbolLiteral + q.format(key.value) + q.text(":") + when DynaSymbol + q.format(key) + q.text(":") + end + end + end + + class Rockets < Base + def format_key(q, key) + case key + when Label + q.text(":") + q.text(key.value.chomp(":")) + when DynaSymbol + q.text(":") + q.format(key) + else + q.format(key) + end + + q.text(" =>") + end + end + + def self.for(container) + labels = + container.assocs.all? do |assoc| + next true if assoc.is_a?(AssocSplat) + + case assoc.key + when Label + true + when SymbolLiteral + # When attempting to convert a hash rocket into a hash label, + # you need to take care because only certain patterns are + # allowed. Ruby source says that they have to match keyword + # arguments to methods, but don't specify what that is. After + # some experimentation, it looks like it's: + value = assoc.key.value.value + value.match?(/^[_A-Za-z]/) && !value.end_with?("=") + when DynaSymbol + true + else + false + end + end + + (labels ? Labels : Rockets).new(container) + end + end + # BareAssocHash represents a hash of contents being passed as a method # argument (and therefore has omitted braces). It's very similar to an # AssocListFromArgs node. @@ -1379,27 +2074,47 @@ def on_backtick(value) # method(key1: value1, key2: value2) # class BareAssocHash - # [Array[AssocNew | AssocSplat]] + # [Array[ AssocNew | AssocSplat ]] attr_reader :assocs # [Location] the location of this node attr_reader :location - def initialize(assocs:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(assocs:, location:, comments: []) @assocs = assocs @location = location + @comments = comments + end + + def child_nodes + assocs + end + + def format(q) + q.format(HashFormatter.for(self)) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('bare_assoc_hash') + q.group(2, "(", ")") do + q.text("bare_assoc_hash") + q.breakable - q.group(2, '(', ')') { q.seplist(assocs) { |assoc| q.pp(assoc) } } + q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :bare_assoc_hash, assocs: assocs, loc: location }.to_json(*opts) + { + type: :bare_assoc_hash, + assocs: assocs, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -1425,34 +2140,64 @@ class Begin # [Location] the location of this node attr_reader :location - def initialize(bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(bodystmt:, location:, comments: []) @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [bodystmt] + end + + def format(q) + q.text("begin") + + unless bodystmt.empty? + q.indent do + q.breakable(force: true) unless bodystmt.statements.empty? + q.format(bodystmt) + end + end + + q.breakable(force: true) + q.text("end") end def pretty_print(q) - q.group(2, '(', ')') do - q.text('begin') + q.group(2, "(", ")") do + q.text("begin") + q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :begin, bodystmt: bodystmt, loc: location }.to_json(*opts) + { + type: :begin, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_begin: (BodyStmt bodystmt) -> Begin def on_begin(bodystmt) - keyword = find_token(Kw, 'begin') + keyword = find_token(Kw, "begin") end_char = if bodystmt.rescue_clause || bodystmt.ensure_clause || bodystmt.else_clause bodystmt.location.end_char else - find_token(Kw, 'end').location.end_char + find_token(Kw, "end").location.end_char end bodystmt.bind(keyword.location.end_char, end_char) @@ -1477,7 +2222,7 @@ class Binary # [untyped] the left-hand side of the expression attr_reader :left - # [String] the operator used between the two expressions + # [Symbol] the operator used between the two expressions attr_reader :operator # [untyped] the right-hand side of the expression @@ -1486,22 +2231,50 @@ class Binary # [Location] the location of this node attr_reader :location - def initialize(left:, operator:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, operator:, right:, location:, comments: []) @left = left @operator = operator @right = right @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + def format(q) + power = operator == :** + + q.group do + q.group { q.format(left) } + q.text(" ") unless power + q.text(operator) + + q.indent do + q.breakable(power ? "" : " ") + q.format(right) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('binary') + q.group(2, "(", ")") do + q.text("binary") + q.breakable q.pp(left) + q.breakable q.text(operator) + q.breakable q.pp(right) + + q.pp(Comment::List.new(comments)) end end @@ -1511,7 +2284,8 @@ def to_json(*opts) left: left, op: operator, right: right, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -1524,9 +2298,7 @@ def on_binary(left, operator, right) # `operator` object would be `:<`. However, on JRuby, it's an `@op` node, # so here we're going to explicitly convert it into the same normalized # form. - unless operator.is_a?(Symbol) - operator = tokens.delete(operator).value - end + operator = tokens.delete(operator).value unless operator.is_a?(Symbol) Binary.new( left: left, @@ -1536,6 +2308,52 @@ def on_binary(left, operator, right) ) end + # This module will remove any breakables from the list of contents so that no + # newlines are present in the output. + module RemoveBreaks + class << self + def call(doc) + marker = Object.new + stack = [doc] + + while stack.any? + doc = stack.pop + + if doc == marker + stack.pop + next + end + + stack += [doc, marker] + + case doc + when PrettyPrint::Align, PrettyPrint::Indent, PrettyPrint::Group + doc.contents.map! { |child| remove_breaks(child) } + stack += doc.contents.reverse + when PrettyPrint::IfBreak + doc.flat_contents.map! { |child| remove_breaks(child) } + stack += doc.flat_contents.reverse + end + end + end + + private + + def remove_breaks(doc) + case doc + when PrettyPrint::Breakable + text = PrettyPrint::Text.new + text.add(object: doc.force? ? "; " : doc.separator, width: doc.width) + text + when PrettyPrint::IfBreak + PrettyPrint::Align.new(indent: 0, contents: doc.flat_contents) + else + doc + end + end + end + end + # BlockVar represents the parameters being declared for a block. Effectively # this node is everything contained within the pipes. This includes all of the # various parameter types, as well as block-local variable declarations. @@ -1547,28 +2365,51 @@ class BlockVar # [Params] the parameters being declared with the block attr_reader :params - # [Array[Ident]] the list of block-local variable declarations + # [Array[ Ident ]] the list of block-local variable declarations attr_reader :locals # [Location] the location of this node attr_reader :location - def initialize(params:, locals:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(params:, locals:, location:, comments: []) @params = params @locals = locals @location = location + @comments = comments + end + + def child_nodes + [params, *locals] + end + + def format(q) + q.group(0, "|", "|") do + doc = q.format(params) + RemoveBreaks.call(doc) + + if locals.any? + q.text("; ") + q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) } + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('block_var') + q.group(2, "(", ")") do + q.text("block_var") + q.breakable q.pp(params) if locals.any? q.breakable - q.group(2, '(', ')') { q.seplist(locals) { |local| q.pp(local) } } + q.group(2, "(", ")") { q.seplist(locals) { |local| q.pp(local) } } end + + q.pp(Comment::List.new(comments)) end end @@ -1577,7 +2418,8 @@ def to_json(*opts) type: :block_var, params: params, locals: locals, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -1612,28 +2454,46 @@ class BlockArg # [Location] the location of this node attr_reader :location - def initialize(name:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, location:, comments: []) @name = name @location = location + @comments = comments + end + + def child_nodes + [name] + end + + def format(q) + q.text("&") + q.format(name) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('blockarg') + q.group(2, "(", ")") do + q.text("blockarg") + q.breakable q.pp(name) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :blockarg, name: name, loc: location }.to_json(*opts) + { type: :blockarg, name: name, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_blockarg: (Ident name) -> BlockArg def on_blockarg(name) - operator = find_token(Op, '&') + operator = find_token(Op, "&") BlockArg.new(name: name, location: operator.location.to(name.location)) end @@ -1657,18 +2517,23 @@ class BodyStmt # [Location] the location of this node attr_reader :location + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + def initialize( statements:, rescue_clause:, else_clause:, ensure_clause:, - location: + location:, + comments: [] ) @statements = statements @rescue_clause = rescue_clause @else_clause = else_clause @ensure_clause = ensure_clause @location = location + @comments = comments end def bind(start_char, end_char) @@ -1698,9 +2563,47 @@ def bind(start_char, end_char) end end + def empty? + statements.empty? && !rescue_clause && !else_clause && !ensure_clause + end + + def child_nodes + [statements, rescue_clause, else_clause, ensure_clause] + end + + def format(q) + q.group do + q.format(statements) unless statements.empty? + + if rescue_clause + q.nest(-2) do + q.breakable(force: true) + q.format(rescue_clause) + end + end + + if else_clause + q.nest(-2) do + q.breakable(force: true) + q.text("else") + end + q.breakable(force: true) + q.format(else_clause) + end + + if ensure_clause + q.nest(-2) do + q.breakable(force: true) + q.format(ensure_clause) + end + end + end + end + def pretty_print(q) - q.group(2, '(', ')') do - q.text('bodystmt') + q.group(2, "(", ")") do + q.text("bodystmt") + q.breakable q.pp(statements) @@ -1718,6 +2621,8 @@ def pretty_print(q) q.breakable q.pp(ensure_clause) end + + q.pp(Comment::List.new(comments)) end end @@ -1728,7 +2633,8 @@ def to_json(*opts) rsc: rescue_clause, els: else_clause, ens: ensure_clause, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -1750,6 +2656,86 @@ def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) ) end + # Responsible for formatting either a BraceBlock or a DoBlock. + class BlockFormatter + class BlockOpenFormatter + # [String] the actual output that should be printed + attr_reader :text + + # [LBrace | Keyword] the node that is being represented + attr_reader :node + + def initialize(text, node) + @text = text + @node = node + end + + def comments + node.comments + end + + def format(q) + q.text(text) + end + end + + # [BraceBlock | DoBlock] the block node to be formatted + attr_reader :node + + # [LBrace | Keyword] the node that opens the block + attr_reader :block_open + + # [BodyStmt | Statements] the statements inside the block + attr_reader :statements + + def initialize(node, block_open, statements) + @node = node + @block_open = block_open + @statements = statements + end + + def format(q) + q.group do + q.text(" ") + + q.if_break do + q.format(BlockOpenFormatter.new("do", block_open)) + + if node.block_var + q.text(" ") + q.format(node.block_var) + end + + unless statements.empty? + q.indent do + q.breakable + q.format(statements) + end + end + + q.breakable + q.text("end") + end.if_flat do + q.format(BlockOpenFormatter.new("{", block_open)) + + if node.block_var + q.breakable + q.format(node.block_var) + q.breakable + end + + unless statements.empty? + q.breakable unless node.block_var + q.format(statements) + q.breakable + end + + q.text("}") + end + end + end + end + # BraceBlock represents passing a block to a method call using the { } # operators. # @@ -1768,16 +2754,28 @@ class BraceBlock # [Location] the location of this node attr_reader :location - def initialize(lbrace:, block_var:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, block_var:, statements:, location:, comments: []) @lbrace = lbrace @block_var = block_var @statements = statements @location = location + @comments = comments + end + + def child_nodes + [lbrace, block_var, statements] + end + + def format(q) + BlockFormatter.new(self, lbrace, statements).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('brace_block') + q.group(2, "(", ")") do + q.text("brace_block") if block_var q.breakable @@ -1786,6 +2784,8 @@ def pretty_print(q) q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -1795,7 +2795,8 @@ def to_json(*opts) lbrace: lbrace, block_var: block_var, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -1830,6 +2831,37 @@ def on_brace_block(block_var, statements) ) end + # Formats either a Break or Next node. + class FlowControlFormatter + # [String] the keyword to print + attr_reader :keyword + + # [Break | Next] the node being formatted + attr_reader :node + + def initialize(keyword, node) + @keyword = keyword + @node = node + end + + def format(q) + arguments = node.arguments + + q.group do + q.text(keyword) + + if arguments.parts.any? + if arguments.parts.length == 1 && arguments.parts.first.is_a?(Paren) + q.format(arguments) + else + q.text(" ") + q.nest(keyword.length + 1) { q.format(arguments) } + end + end + end + end + end + # Break represents using the +break+ keyword. # # break @@ -1839,41 +2871,81 @@ def on_brace_block(block_var, statements) # break 1 # class Break - # [Args | ArgsAddBlock] the arguments being sent to the keyword + # [Args] the arguments being sent to the keyword attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + def format(q) + FlowControlFormatter.new("break", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('break') + q.group(2, "(", ")") do + q.text("break") + q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :break, args: arguments, loc: location }.to_json(*opts) + { type: :break, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_break: ((Args | ArgsAddBlock) arguments) -> Break + # on_break: (Args arguments) -> Break def on_break(arguments) - keyword = find_token(Kw, 'break') + keyword = find_token(Kw, "break") location = keyword.location - location = location.to(arguments.location) unless arguments.is_a?(Args) + location = location.to(arguments.location) if arguments.parts.any? Break.new(arguments: arguments, location: location) end + # Wraps a call operator (which can be a string literal :: or an Op node or a + # Period node) and formats it when called. + class CallOperatorFormatter + # [:"::" | Op | Period] the operator being formatted + attr_reader :operator + + def initialize(operator) + @operator = operator + end + + def comments + operator == :"::" ? [] : operator.comments + end + + def format(q) + if operator == :"::" || (operator.is_a?(Op) && operator.value == "::") + q.text(".") + else + operator.format(q) + end + end + end + # Call represents a method call. This node doesn't contain the arguments being # passed (if arguments are passed, this node will get nested under a # MethodAddArg node). @@ -1893,22 +2965,47 @@ class Call # [Location] the location of this node attr_reader :location - def initialize(receiver:, operator:, message:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(receiver:, operator:, message:, location:, comments: []) @receiver = receiver @operator = operator @message = message @location = location + @comments = comments + end + + def child_nodes + [receiver, (operator if operator != :"::"), (message if message != :call)] + end + + def format(q) + q.group do + q.format(receiver) + q.group do + q.indent do + q.format(CallOperatorFormatter.new(operator)) + q.format(message) if message != :call + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('call') + q.group(2, "(", ")") do + q.text("call") + q.breakable q.pp(receiver) + q.breakable q.pp(operator) + q.breakable q.pp(message) + + q.pp(Comment::List.new(comments)) end end @@ -1918,7 +3015,8 @@ def to_json(*opts) receiver: receiver, op: operator, message: message, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -1968,15 +3066,36 @@ class Case # [Location] the location of this node attr_reader :location - def initialize(value:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, consequent:, location:, comments: []) @value = value @consequent = consequent @location = location + @comments = comments + end + + def child_nodes + [value, consequent] + end + + def format(q) + q.group(0, "case", "end") do + if value + q.text(" ") + q.format(value) + end + + q.breakable(force: true) + q.format(consequent) + q.breakable(force: true) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('case') + q.group(2, "(", ")") do + q.text("case") if value q.breakable @@ -1985,13 +3104,19 @@ def pretty_print(q) q.breakable q.pp(consequent) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :case, value: value, cons: consequent, loc: location }.to_json( - *opts - ) + { + type: :case, + value: value, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -2014,16 +3139,38 @@ class RAssign # [Location] the location of this node attr_reader :location - def initialize(value:, operator:, pattern:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, operator:, pattern:, location:, comments: []) @value = value @operator = operator @pattern = pattern @location = location + @comments = comments + end + + def child_nodes + [value, operator, pattern] + end + + def format(q) + q.group do + q.format(value) + q.text(" ") + q.format(operator) + q.group do + q.indent do + q.breakable + q.format(pattern) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('rassign') + q.group(2, "(", ")") do + q.text("rassign") q.breakable q.pp(value) @@ -2033,6 +3180,8 @@ def pretty_print(q) q.breakable q.pp(pattern) + + q.pp(Comment::List.new(comments)) end end @@ -2042,7 +3191,8 @@ def to_json(*opts) value: value, op: operator, pattern: pattern, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -2050,7 +3200,7 @@ def to_json(*opts) # :call-seq: # on_case: (untyped value, untyped consequent) -> Case | RAssign def on_case(value, consequent) - if keyword = find_token(Kw, 'case', consume: false) + if keyword = find_token(Kw, "case", consume: false) tokens.delete(keyword) Case.new( @@ -2059,9 +3209,7 @@ def on_case(value, consequent) location: keyword.location.to(consequent.location) ) else - operator = - find_token(Kw, 'in', consume: false) || - find_token(Op, '=>') + operator = find_token(Kw, "in", consume: false) || find_token(Op, "=>") RAssign.new( value: value, @@ -2118,16 +3266,58 @@ class ClassDeclaration # [Location] the location of this node attr_reader :location - def initialize(constant:, superclass:, bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, superclass:, bodystmt:, location:, comments: []) @constant = constant @superclass = superclass @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [constant, superclass, bodystmt] + end + + def format(q) + declaration = -> do + q.group do + q.text("class ") + q.format(constant) + + if superclass + q.text(" < ") + q.format(superclass) + end + end + end + + if bodystmt.empty? + q.group do + declaration.call + q.breakable(force: true) + q.text("end") + end + else + q.group do + declaration.call + + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + + q.breakable(force: true) + q.text("end") + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('class') + q.group(2, "(", ")") do + q.text("class") q.breakable q.pp(constant) @@ -2139,6 +3329,8 @@ def pretty_print(q) q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end @@ -2148,7 +3340,8 @@ def to_json(*opts) constant: constant, superclass: superclass, bodystmt: bodystmt, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -2160,8 +3353,8 @@ def to_json(*opts) # BodyStmt bodystmt # ) -> ClassDeclaration def on_class(constant, superclass, bodystmt) - beginning = find_token(Kw, 'class') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "class") + ending = find_token(Kw, "end") bodystmt.bind( find_next_statement_start((superclass || constant).location.end_char), @@ -2181,24 +3374,12 @@ class Comma # [String] the comma in the string attr_reader :value - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('comma') - q.breakable - q.pp(value) - end - end + # [Location] the location of this node + attr_reader :location - def to_json(*opts) - { type: :@comma, value: value, loc: location }.to_json(*opts) + def initialize(value:, location:) + @value = value + @location = location end end @@ -2225,27 +3406,45 @@ class Command # [Const | Ident] the message being sent to the implicit receiver attr_reader :message - # [Args | ArgsAddBlock] the arguments being sent with the message + # [Args] the arguments being sent with the message attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(message:, arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(message:, arguments:, location:, comments: []) @message = message @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [message, arguments] + end + + def format(q) + q.group do + q.format(message) + q.text(" ") + q.nest(message.value.length + 1) { q.format(arguments) } + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('command') + q.group(2, "(", ")") do + q.text("command") q.breakable q.pp(message) q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end @@ -2254,16 +3453,14 @@ def to_json(*opts) type: :command, message: message, args: arguments, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end # :call-seq: - # on_command: ( - # (Const | Ident) message, - # (Args | ArgsAddBlock) arguments - # ) -> Command + # on_command: ((Const | Ident) message, Args arguments) -> Command def on_command(message, arguments) Command.new( message: message, @@ -2287,23 +3484,54 @@ class CommandCall # [Const | Ident | Op] the message being send attr_reader :message - # [Args | ArgsAddBlock] the arguments going along with the message + # [Args] the arguments going along with the message attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(receiver:, operator:, message:, arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + receiver:, + operator:, + message:, + arguments:, + location:, + comments: [] + ) @receiver = receiver @operator = operator @message = message @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [receiver, message, arguments] + end + + def format(q) + q.group do + doc = q.format(receiver) + q.format(CallOperatorFormatter.new(operator)) + q.format(message) + q.text(" ") + + width = doc_width(doc) + if width > (q.maxwidth / 2) || width < 2 + q.indent { q.format(arguments) } + else + q.nest(width) { q.format(arguments) } + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('case') + q.group(2, "(", ")") do + q.text("command_call") q.breakable q.pp(receiver) @@ -2316,6 +3544,8 @@ def pretty_print(q) q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end @@ -2326,9 +3556,37 @@ def to_json(*opts) op: operator, message: message, args: arguments, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end + + private + + # This is a somewhat naive method that is attempting to sum up the width of + # the doc nodes that make up the given doc node. This is used to align + # content. + def doc_width(parent) + queue = [parent] + width = 0 + + until queue.empty? + doc = queue.shift + + case doc + when PrettyPrint::Text + width += doc.width + when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group + queue += doc.contents.reverse + when PrettyPrint::IfBreak + queue += doc.flat_contents.reverse + when PrettyPrint::Breakable + width = doc.force? ? 0 : width + doc.width + end + end + + width + end end # :call-seq: @@ -2336,7 +3594,7 @@ def to_json(*opts) # untyped receiver, # (:"::" | Op | Period) operator, # (Const | Ident | Op) message, - # (Args | ArgsAddBlock) arguments + # Args arguments # ) -> CommandCall def on_command_call(receiver, operator, message, arguments) ending = arguments || message @@ -2355,12 +3613,29 @@ def on_command_call(receiver, operator, message, arguments) # # comment # class Comment + class List + # [Array[ Comment ]] the list of comments this list represents + attr_reader :comments + + def initialize(comments) + @comments = comments + end + + def pretty_print(q) + return if comments.empty? + + q.breakable + q.group(2, "(", ")") { q.seplist(comments) { |comment| q.pp(comment) } } + end + end + # [String] the contents of the comment attr_reader :value # [boolean] whether or not there is code on the same line as this comment. # If there is, then inline will be true. attr_reader :inline + alias inline? inline # [Location] the location of this node attr_reader :location @@ -2369,11 +3644,39 @@ def initialize(value:, inline:, location:) @value = value @inline = inline @location = location + + @leading = false + @trailing = false + end + + def leading! + @leading = true + end + + def leading? + @leading + end + + def trailing! + @trailing = true + end + + def trailing? + @trailing + end + + def comments + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('comment') + q.group(2, "(", ")") do + q.text("comment") + q.breakable q.pp(value) end @@ -2381,8 +3684,8 @@ def pretty_print(q) def to_json(*opts) { - type: :@comment, - value: value.force_encoding('UTF-8'), + type: :comment, + value: value.force_encoding("UTF-8"), inline: inline, loc: location }.to_json(*opts) @@ -2395,8 +3698,8 @@ def on_comment(value) line = lineno comment = Comment.new( - value: value[1..-1].chomp, - inline: value.strip != lines[line - 1], + value: value.chomp, + inline: value.strip != lines[line - 1].strip, location: Location.token(line: line, char: char_pos, size: value.size - 1) ) @@ -2426,35 +3729,48 @@ class Const # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('const') + q.group(2, "(", ")") do + q.text("const") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@const, value: value, loc: location }.to_json(*opts) + { type: :const, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_const: (String value) -> Const def on_const(value) - node = - Const.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + Const.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # ConstPathField represents the child node of some kind of assignment. It @@ -2473,21 +3789,37 @@ class ConstPathField # [Location] the location of this node attr_reader :location - def initialize(parent:, constant:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parent:, constant:, location:, comments: []) @parent = parent @constant = constant @location = location + @comments = comments + end + + def child_nodes + [parent, constant] + end + + def format(q) + q.format(parent) + q.text("::") + q.format(constant) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('const_path_field') + q.group(2, "(", ")") do + q.text("const_path_field") q.breakable q.pp(parent) q.breakable q.pp(constant) + + q.pp(Comment::List.new(comments)) end end @@ -2496,7 +3828,8 @@ def to_json(*opts) type: :const_path_field, parent: parent, constant: constant, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -2525,21 +3858,37 @@ class ConstPathRef # [Location] the location of this node attr_reader :location - def initialize(parent:, constant:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parent:, constant:, location:, comments: []) @parent = parent @constant = constant @location = location + @comments = comments + end + + def child_nodes + [parent, constant] + end + + def format(q) + q.format(parent) + q.text("::") + q.format(constant) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('const_path_ref') + q.group(2, "(", ")") do + q.text("const_path_ref") q.breakable q.pp(parent) q.breakable q.pp(constant) + + q.pp(Comment::List.new(comments)) end end @@ -2548,7 +3897,8 @@ def to_json(*opts) type: :const_path_ref, parent: parent, constant: constant, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -2576,22 +3926,41 @@ class ConstRef # [Location] the location of this node attr_reader :location - def initialize(constant:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, location:, comments: []) @constant = constant @location = location + @comments = comments + end + + def child_nodes + [constant] + end + + def format(q) + q.format(constant) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('const_ref') + q.group(2, "(", ")") do + q.text("const_ref") q.breakable q.pp(constant) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :const_ref, constant: constant, loc: location }.to_json(*opts) + { + type: :const_ref, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -2612,44 +3981,56 @@ class CVar # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('cvar') + q.group(2, "(", ")") do + q.text("cvar") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@cvar, value: value, loc: location }.to_json(*opts) + { type: :cvar, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_cvar: (String value) -> CVar def on_cvar(value) - node = - CVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + CVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # Def represents defining a regular method on the current self object. # - # def method(param) do result end + # def method(param) result end # class Def - # [Backtick | Const | Ident | Keyword | Op] the name of the method + # [Backtick | Const | Ident | Kw | Op] the name of the method attr_reader :name # [Params | Paren] the parameter declaration for the method @@ -2661,16 +4042,51 @@ class Def # [Location] the location of this node attr_reader :location - def initialize(name:, params:, bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, params:, bodystmt:, location:, comments: []) @name = name @params = params @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [name, params, bodystmt] + end + + def format(q) + q.group do + q.group do + q.text("def ") + q.format(name) + + if params.is_a?(Params) && !params.empty? + q.text("(") + q.format(params) + q.text(")") + else + q.format(params) + end + end + + unless bodystmt.empty? + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + end + + q.breakable(force: true) + q.text("end") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('def') + q.group(2, "(", ")") do + q.text("def") q.breakable q.pp(name) @@ -2680,6 +4096,8 @@ def pretty_print(q) q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end @@ -2689,7 +4107,8 @@ def to_json(*opts) name: name, params: params, bodystmt: bodystmt, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -2699,10 +4118,10 @@ def to_json(*opts) # def method = result # class DefEndless - # [Backtick | Const | Ident | Keyword | Op] the name of the method + # [Backtick | Const | Ident | Kw | Op] the name of the method attr_reader :name - # [Paren] the parameter declaration for the method + # [nil | Paren] the parameter declaration for the method attr_reader :paren # [untyped] the expression to be executed by the method @@ -2711,43 +4130,71 @@ class DefEndless # [Location] the location of this node attr_reader :location - def initialize(name:, paren:, statement:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, paren:, statement:, location:, comments: []) @name = name @paren = paren @statement = statement @location = location + @comments = comments + end + + def child_nodes + [name, paren, statement] + end + + def format(q) + q.group do + q.text("def ") + q.format(name) + q.format(paren) if paren && !paren.contents.empty? + q.text(" =") + q.group do + q.indent do + q.breakable + q.format(statement) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('defsl') + q.group(2, "(", ")") do + q.text("def_endless") q.breakable q.pp(name) - q.breakable - q.pp(paren) + if paren + q.breakable + q.pp(paren) + end q.breakable q.pp(statement) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) { - type: :defsl, + type: :def_endless, name: name, paren: paren, stmt: statement, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end # :call-seq: # on_def: ( - # (Backtick | Const | Ident | Keyword | Op) name, - # (Params | Paren) params, + # (Backtick | Const | Ident | Kw | Op) name, + # (nil | Params | Paren) params, # untyped bodystmt # ) -> Def | DefEndless def on_def(name, params, bodystmt) @@ -2757,7 +4204,7 @@ def on_def(name, params, bodystmt) # Find the beginning of the method definition, which works for single-line # and normal method definitions. - beginning = find_token(Kw, 'def') + beginning = find_token(Kw, "def") # If we don't have a bodystmt node, then we have a single-line method unless bodystmt.is_a?(BodyStmt) @@ -2787,7 +4234,7 @@ def on_def(name, params, bodystmt) params = Params.new(location: location) end - ending = find_token(Kw, 'end') + ending = find_token(Kw, "end") bodystmt.bind( find_next_statement_start(params.location.end_char), ending.location.start_char @@ -2813,33 +4260,55 @@ class Defined # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.group(0, "defined?(", ")") do + q.indent do + q.breakable("") + q.format(value) + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('defined') + q.group(2, "(", ")") do + q.text("defined") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :defined, value: value, loc: location }.to_json(*opts) + { type: :defined, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_defined: (untyped value) -> Defined def on_defined(value) - beginning = find_token(Kw, 'defined?') + beginning = find_token(Kw, "defined?") ending = value range = beginning.location.end_char...value.location.start_char - if source[range].include?('(') + if source[range].include?("(") find_token(LParen) ending = find_token(RParen) end @@ -2849,7 +4318,7 @@ def on_defined(value) # Defs represents defining a singleton method on an object. # - # def object.method(param) do result end + # def object.method(param) result end # class Defs # [untyped] the target where the method is being defined @@ -2858,7 +4327,7 @@ class Defs # [Op | Period] the operator being used to declare the method attr_reader :operator - # [Backtick | Const | Ident | Keyword | Op] the name of the method + # [Backtick | Const | Ident | Kw | Op] the name of the method attr_reader :name # [Params | Paren] the parameter declaration for the method @@ -2870,18 +4339,63 @@ class Defs # [Location] the location of this node attr_reader :location - def initialize(target:, operator:, name:, params:, bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + target:, + operator:, + name:, + params:, + bodystmt:, + location:, + comments: [] + ) @target = target @operator = operator @name = name @params = params @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [target, operator, name, params, bodystmt] + end + + def format(q) + q.group do + q.group do + q.text("def ") + q.format(target) + q.format(CallOperatorFormatter.new(operator)) + q.format(name) + + if params.is_a?(Params) && !params.empty? + q.text("(") + q.format(params) + q.text(")") + else + q.format(params) + end + end + + unless bodystmt.empty? + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + end + + q.breakable(force: true) + q.text("end") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('defs') + q.group(2, "(", ")") do + q.text("defs") q.breakable q.pp(target) @@ -2897,6 +4411,8 @@ def pretty_print(q) q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end @@ -2908,7 +4424,8 @@ def to_json(*opts) name: name, params: params, bodystmt: bodystmt, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -2917,7 +4434,7 @@ def to_json(*opts) # on_defs: ( # untyped target, # (Op | Period) operator, - # (Backtick | Const | Ident | Keyword | Op) name, + # (Backtick | Const | Ident | Kw | Op) name, # (Params | Paren) params, # BodyStmt bodystmt # ) -> Defs @@ -2942,8 +4459,8 @@ def on_defs(target, operator, name, params, bodystmt) params = Params.new(location: location) end - beginning = find_token(Kw, 'def') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "def") + ending = find_token(Kw, "end") bodystmt.bind( find_next_statement_start(params.location.end_char), @@ -2979,16 +4496,28 @@ class DoBlock # [Location] the location of this node attr_reader :location - def initialize(keyword:, block_var:, bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(keyword:, block_var:, bodystmt:, location:, comments: []) @keyword = keyword @block_var = block_var @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [keyword, block_var, bodystmt] + end + + def format(q) + BlockFormatter.new(self, keyword, bodystmt).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('do_block') + q.group(2, "(", ")") do + q.text("do_block") if block_var q.breakable @@ -2997,6 +4526,8 @@ def pretty_print(q) q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end @@ -3006,7 +4537,8 @@ def to_json(*opts) keyword: keyword, block_var: block_var, bodystmt: bodystmt, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -3014,8 +4546,8 @@ def to_json(*opts) # :call-seq: # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock def on_do_block(block_var, bodystmt) - beginning = find_token(Kw, 'do') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "do") + ending = find_token(Kw, "end") bodystmt.bind( find_next_statement_start((block_var || beginning).location.end_char), @@ -3030,6 +4562,34 @@ def on_do_block(block_var, bodystmt) ) end + # Responsible for formatting Dot2 and Dot3 nodes. + class DotFormatter + # [String] the operator to display + attr_reader :operator + + # [Dot2 | Dot3] the node that is being formatter + attr_reader :node + + def initialize(operator, node) + @operator = operator + @node = node + end + + def format(q) + parent = q.parent + space = parent.is_a?(If) || parent.is_a?(Unless) + + left = node.left + right = node.right + + q.format(left) if left + q.text(" ") if space + q.text(operator) + q.text(" ") if space + q.format(right) if right + end + end + # Dot2 represents using the .. operator between two expressions. Usually this # is to create a range object. # @@ -3051,15 +4611,27 @@ class Dot2 # [Location] the location of this node attr_reader :location - def initialize(left:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) @left = left @right = right @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + def format(q) + DotFormatter.new("..", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('dot2') + q.group(2, "(", ")") do + q.text("dot2") if left q.breakable @@ -3070,18 +4642,26 @@ def pretty_print(q) q.breakable q.pp(right) end + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :dot2, left: left, right: right, loc: location }.to_json(*opts) + { + type: :dot2, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2 def on_dot2(left, right) - operator = find_token(Op, '..') + operator = find_token(Op, "..") beginning = left || operator ending = right || operator @@ -3115,15 +4695,27 @@ class Dot3 # [Location] the location of this node attr_reader :location - def initialize(left:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) @left = left @right = right @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + def format(q) + DotFormatter.new("...", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('dot3') + q.group(2, "(", ")") do + q.text("dot3") if left q.breakable @@ -3134,27 +4726,76 @@ def pretty_print(q) q.breakable q.pp(right) end + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :dot3, left: left, right: right, loc: location }.to_json(*opts) + { + type: :dot3, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> Dot3 def on_dot3(left, right) - operator = find_token(Op, '...') + operator = find_token(Op, "...") + + beginning = left || operator + ending = right || operator + + Dot3.new( + left: left, + right: right, + location: beginning.location.to(ending.location) + ) + end + + # Responsible for providing information about quotes to be used for strings + # and dynamic symbols. + module Quotes + # The matching pairs of quotes that can be used with % literals. + PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze + + # If there is some part of this string that matches an escape sequence or + # that contains the interpolation pattern ("#{"), then we are locked into + # whichever quote the user chose. (If they chose single quotes, then double + # quoting would activate the escape sequence, and if they chose double + # quotes, then single quotes would deactivate it.) + def self.locked?(node) + node.parts.any? do |part| + part.is_a?(TStringContent) && part.value.match?(/#[@${]|[\\]/) + end + end + + # Find the matching closing quote for the given opening quote. + def self.matching(quote) + PAIRS.fetch(quote) { quote } + end - beginning = left || operator - ending = right || operator + # Escape and unescape single and double quotes as needed to be able to + # enclose +content+ with +enclosing+. + def self.normalize(content, enclosing) + return content if enclosing != "\"" && enclosing != "'" - Dot3.new( - left: left, - right: right, - location: beginning.location.to(ending.location) - ) + content.gsub(/\\([\s\S])|(['"])/) do + _match, escaped, quote = Regexp.last_match.to_a + + if quote == enclosing + "\\#{quote}" + elsif quote + quote + else + "\\#{escaped}" + end + end + end end # DynaSymbol represents a symbol literal that uses quotes to dynamically @@ -3167,7 +4808,7 @@ def on_dot3(left, right) # { "#{key}": value } # class DynaSymbol - # [Array[StringDVar | StringEmbExpr | TStringContent]] the parts of the + # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the # dynamic symbol attr_reader :parts @@ -3177,25 +4818,96 @@ class DynaSymbol # [Location] the location of this node attr_reader :location - def initialize(parts:, quote:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, quote:, location:, comments: []) @parts = parts @quote = quote @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + opening_quote, closing_quote = quotes(q) + + q.group(0, opening_quote, closing_quote) do + parts.each do |part| + if part.is_a?(TStringContent) + value = Quotes.normalize(part.value, closing_quote) + separator = -> { q.breakable(force: true, indent: false) } + q.seplist(value.split(/\r?\n/, -1), separator) do |text| + q.text(text) + end + else + q.format(part) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('dyna_symbol') + q.group(2, "(", ")") do + q.text("dyna_symbol") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :dyna_symbol, parts: parts, quote: quote, loc: location }.to_json( - *opts - ) + { + type: :dyna_symbol, + parts: parts, + quote: quote, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + # Here we determine the quotes to use for a dynamic symbol. It's bound by a + # lot of rules because it could be in many different contexts with many + # different kinds of escaping. + def quotes(q) + # If we're inside of an assoc node as the key, then it will handle + # printing the : on its own since it could change sides. + parent = q.parent + hash_key = parent.is_a?(Assoc) && parent.key == self + + if quote.start_with?("%s") + # Here we're going to check if there is a closing character, a new line, + # or a quote in the content of the dyna symbol. If there is, then + # quoting could get weird, so just bail out and stick to the original + # quotes in the source. + matching = Quotes.matching(quote[2]) + pattern = /[\n#{Regexp.escape(matching)}'"]/ + + if parts.any? do |part| + part.is_a?(TStringContent) && part.value.match?(pattern) + end + [quote, matching] + elsif Quotes.locked?(self) + ["#{":" unless hash_key}'", "'"] + else + ["#{":" unless hash_key}#{q.quote}", q.quote] + end + elsif Quotes.locked?(self) + if quote.start_with?(":") + [hash_key ? quote[1..-1] : quote, quote[1..-1]] + else + [hash_key ? quote : ":#{quote}", quote] + end + else + [hash_key ? q.quote : ":#{q.quote}", q.quote] + end end end @@ -3238,29 +4950,54 @@ class Else # [Location] the location of this node attr_reader :location - def initialize(statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statements:, location:, comments: []) @statements = statements @location = location + @comments = comments + end + + def child_nodes + [statements] + end + + def format(q) + q.group do + q.text("else") + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('else') + q.group(2, "(", ")") do + q.text("else") q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :else, stmts: statements, loc: location }.to_json(*opts) + { type: :else, stmts: statements, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_else: (Statements statements) -> Else def on_else(statements) - beginning = find_token(Kw, 'else') + beginning = find_token(Kw, "else") # else can either end with an end keyword (in which case we'll want to # consume that event) or it can end with an ensure keyword (in which case @@ -3271,7 +5008,7 @@ def on_else(statements) end node = tokens[index] - ending = node.value == 'end' ? tokens.delete_at(index) : node + ending = node.value == "end" ? tokens.delete_at(index) : node statements.bind(beginning.location.end_char, ending.location.start_char) @@ -3300,16 +5037,53 @@ class Elsif # [Location] the location of this node attr_reader :location - def initialize(predicate:, statements:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + predicate:, + statements:, + consequent:, + location:, + comments: [] + ) @predicate = predicate @statements = statements @consequent = consequent @location = location + @comments = comments + end + + def child_nodes + [predicate, statements, consequent] + end + + def format(q) + q.group do + q.group do + q.text("elsif ") + q.nest("elsif".length - 1) { q.format(predicate) } + end + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.group do + q.breakable(force: true) + q.format(consequent) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('elsif') + q.group(2, "(", ")") do + q.text("elsif") q.breakable q.pp(predicate) @@ -3321,6 +5095,8 @@ def pretty_print(q) q.breakable q.pp(consequent) end + + q.pp(Comment::List.new(comments)) end end @@ -3330,7 +5106,8 @@ def to_json(*opts) pred: predicate, stmts: statements, cons: consequent, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -3342,8 +5119,8 @@ def to_json(*opts) # (nil | Elsif | Else) consequent # ) -> Elsif def on_elsif(predicate, statements, consequent) - beginning = find_token(Kw, 'elsif') - ending = consequent || find_token(Kw, 'end') + beginning = find_token(Kw, "elsif") + ending = consequent || find_token(Kw, "end") statements.bind(predicate.location.end_char, ending.location.start_char) @@ -3374,9 +5151,26 @@ def initialize(value:, location:) @location = location end + def inline? + false + end + + def comments + [] + end + + def child_nodes + [] + end + + def format(q) + q.trim + q.text(value) + end + def pretty_print(q) - q.group(2, '(', ')') do - q.text('embdoc') + q.group(2, "(", ")") do + q.text("embdoc") q.breakable q.pp(value) @@ -3384,7 +5178,7 @@ def pretty_print(q) end def to_json(*opts) - { type: :@embdoc, value: value, loc: location }.to_json(*opts) + { type: :embdoc, value: value, loc: location }.to_json(*opts) end end @@ -3444,19 +5238,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('embexpr_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@embexpr_beg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -3489,19 +5270,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('embexpr_end') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@embexpr_end, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -3536,19 +5304,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('embvar') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@embvar, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -3581,18 +5336,39 @@ class Ensure # [Location] the location of this node attr_reader :location - def initialize(keyword:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(keyword:, statements:, location:, comments: []) @keyword = keyword @statements = statements @location = location + @comments = comments + end + + def child_nodes + [keyword, statements] + end + + def format(q) + q.format(keyword) + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('ensure') + q.group(2, "(", ")") do + q.text("ensure") q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -3601,7 +5377,8 @@ def to_json(*opts) type: :ensure, keyword: keyword, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -3609,11 +5386,11 @@ def to_json(*opts) # :call-seq: # on_ensure: (Statements statements) -> Ensure def on_ensure(statements) - keyword = find_token(Kw, 'ensure') + keyword = find_token(Kw, "ensure") # We don't want to consume the :@kw event, because that would break # def..ensure..end chains. - ending = find_token(Kw, 'end', consume: false) + ending = find_token(Kw, "end", consume: false) statements.bind( find_next_statement_start(keyword.location.end_char), ending.location.start_char @@ -3643,22 +5420,41 @@ class ExcessedComma # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('excessed_comma') + q.group(2, "(", ")") do + q.text("excessed_comma") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :excessed_comma, value: value, loc: location }.to_json(*opts) + { + type: :excessed_comma, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -3687,22 +5483,38 @@ class FCall # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('fcall') + q.group(2, "(", ")") do + q.text("fcall") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :fcall, value: value, loc: location }.to_json(*opts) + { type: :fcall, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -3730,16 +5542,32 @@ class Field # [Location] the location of this node attr_reader :location - def initialize(parent:, operator:, name:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parent:, operator:, name:, location:, comments: []) @parent = parent @operator = operator @name = name @location = location + @comments = comments + end + + def child_nodes + [parent, (operator if operator != :"::"), name] + end + + def format(q) + q.group do + q.format(parent) + q.format(CallOperatorFormatter.new(operator)) + q.format(name) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('field') + q.group(2, "(", ")") do + q.text("field") q.breakable q.pp(parent) @@ -3749,6 +5577,8 @@ def pretty_print(q) q.breakable q.pp(name) + + q.pp(Comment::List.new(comments)) end end @@ -3758,7 +5588,8 @@ def to_json(*opts) parent: parent, op: operator, name: name, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -3789,36 +5620,48 @@ class FloatLiteral # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('float') + q.group(2, "(", ")") do + q.text("float") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@float, value: value, loc: location }.to_json(*opts) + { type: :float, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_float: (String value) -> FloatLiteral def on_float(value) - node = - FloatLiteral.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + FloatLiteral.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # FndPtn represents matching against a pattern where you find a pattern in an @@ -3835,7 +5678,7 @@ class FndPtn # [VarField] the splat on the left-hand side attr_reader :left - # [Array[untyped]] the list of positional expressions in the pattern that + # [Array[ untyped ]] the list of positional expressions in the pattern that # are being matched attr_reader :values @@ -3845,17 +5688,40 @@ class FndPtn # [Location] the location of this node attr_reader :location - def initialize(constant:, left:, values:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, left:, values:, right:, location:, comments: []) @constant = constant @left = left @values = values @right = right @location = location + @comments = comments + end + + def child_nodes + [constant, left, *values, right] + end + + def format(q) + q.format(constant) if constant + q.group(0, "[", "]") do + q.text("*") + q.format(left) + q.comma_breakable + + q.seplist(values) { |value| q.format(value) } + q.comma_breakable + + q.text("*") + q.format(right) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('fndptn') + q.group(2, "(", ")") do + q.text("fndptn") if constant q.breakable @@ -3866,10 +5732,12 @@ def pretty_print(q) q.pp(left) q.breakable - q.group(2, '(', ')') { q.seplist(values) { |value| q.pp(value) } } + q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } q.breakable q.pp(right) + + q.pp(Comment::List.new(comments)) end end @@ -3880,7 +5748,8 @@ def to_json(*opts) left: left, values: values, right: right, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -3911,7 +5780,7 @@ def on_fndptn(constant, left, values, right) # end # class For - # [MLHS | MLHSAddStar | VarField] the variable declaration being used to + # [MLHS | VarField] the variable declaration being used to # pull values out of the object being enumerated attr_reader :index @@ -3924,16 +5793,43 @@ class For # [Location] the location of this node attr_reader :location - def initialize(index:, collection:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(index:, collection:, statements:, location:, comments: []) @index = index @collection = collection @statements = statements @location = location + @comments = comments + end + + def child_nodes + [index, collection, statements] + end + + def format(q) + q.group do + q.text("for ") + q.group { q.format(index) } + q.text(" in ") + q.format(collection) + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + q.breakable(force: true) + q.text("end") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('for') + q.group(2, "(", ")") do + q.text("for") q.breakable q.pp(index) @@ -3943,6 +5839,8 @@ def pretty_print(q) q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -3952,24 +5850,25 @@ def to_json(*opts) index: index, collection: collection, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end # :call-seq: # on_for: ( - # (MLHS | MLHSAddStar | VarField) value, + # (MLHS | VarField) value, # untyped collection, # Statements statements # ) -> For def on_for(index, collection, statements) - beginning = find_token(Kw, 'for') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "for") + ending = find_token(Kw, "end") # Consume the do keyword if it exists so that it doesn't get confused for # some other block - keyword = find_token(Kw, 'do', consume: false) + keyword = find_token(Kw, "do", consume: false) if keyword && keyword.location.start_char > collection.location.end_char && keyword.location.end_char < ending.location.start_char tokens.delete(keyword) @@ -3999,36 +5898,48 @@ class GVar # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('gvar') + q.group(2, "(", ")") do + q.text("gvar") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@gvar, value: value, loc: location }.to_json(*opts) + { type: :gvar, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_gvar: (String value) -> GVar def on_gvar(value) - node = - GVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + GVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # HashLiteral represents a hash literal. @@ -4036,53 +5947,67 @@ def on_gvar(value) # { key => value } # class HashLiteral - # [nil | AssocListFromArgs] the contents of the hash - attr_reader :contents + # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash + attr_reader :assocs # [Location] the location of this node attr_reader :location - def initialize(contents:, location:) - @contents = contents + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(assocs:, location:, comments: []) + @assocs = assocs @location = location + @comments = comments end - def pretty_print(q) - q.group(2, '(', ')') do - q.text('hash') + def child_nodes + assocs + end + def format(q) + contents = -> do + q.text("{") + q.indent do + q.breakable + q.format(HashFormatter.for(self)) + end q.breakable - q.pp(contents) + q.text("}") + end + + q.parent.is_a?(Assoc) ? contents.call : q.group(&contents) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("hash") + + if assocs.any? + q.breakable + q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } } + end + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :hash, cnts: contents, loc: location }.to_json(*opts) + { type: :hash, assocs: assocs, loc: location, cmts: comments }.to_json( + *opts + ) end end - # :call-seq: - # on_hash: ((nil | AssocListFromArgs) contents) -> HashLiteral - def on_hash(contents) - lbrace = find_token(LBrace) - rbrace = find_token(RBrace) - - if contents - # Here we're going to expand out the location information for the contents - # node so that it can grab up any remaining comments inside the hash. - location = - Location.new( - start_line: contents.location.start_line, - start_char: lbrace.location.end_char, - end_line: contents.location.end_line, - end_char: rbrace.location.start_char - ) - - contents = contents.class.new(assocs: contents.assocs, location: location) - end + # :call-seq: + # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral + def on_hash(assocs) + lbrace = find_token(LBrace) + rbrace = find_token(RBrace) HashLiteral.new( - contents: contents, + assocs: assocs || [], location: lbrace.location.to(rbrace.location) ) end @@ -4100,26 +6025,67 @@ class Heredoc # [String] the ending of the heredoc attr_reader :ending - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the # heredoc string literal attr_reader :parts # [Location] the location of this node attr_reader :location - def initialize(beginning:, ending: nil, parts: [], location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, ending: nil, parts: [], location:, comments: []) @beginning = beginning @ending = ending @parts = parts @location = location + @comments = comments + end + + def child_nodes + [beginning, *parts] + end + + def format(q) + # This is a very specific behavior that should probably be included in the + # prettyprint module. It's when you want to force a newline, but don't + # want to force the break parent. + breakable = -> do + q.target << + PrettyPrint::Breakable.new(" ", 1, indent: false, force: true) + end + + q.group do + q.format(beginning) + + q.line_suffix do + q.group do + breakable.call + + parts.each do |part| + if part.is_a?(TStringContent) + texts = part.value.split(/\r?\n/, -1) + q.seplist(texts, breakable) { |text| q.text(text) } + else + q.format(part) + end + end + + q.text(ending) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('heredoc') + q.group(2, "(", ")") do + q.text("heredoc") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end @@ -4129,7 +6095,8 @@ def to_json(*opts) beging: beginning, ending: ending, parts: parts, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4148,22 +6115,41 @@ class HeredocBeg # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('heredoc_beg') + q.group(2, "(", ")") do + q.text("heredoc_beg") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@heredoc_beg, value: value, loc: location }.to_json(*opts) + { + type: :heredoc_beg, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -4186,13 +6172,12 @@ def on_heredoc_beg(value) def on_heredoc_dedent(string, width) heredoc = @heredocs[-1] - @heredocs[-1] = - Heredoc.new( - beginning: heredoc.beginning, - ending: heredoc.ending, - parts: string.parts, - location: heredoc.location - ) + @heredocs[-1] = Heredoc.new( + beginning: heredoc.beginning, + ending: heredoc.ending, + parts: string.parts, + location: heredoc.location + ) end # :call-seq: @@ -4200,19 +6185,18 @@ def on_heredoc_dedent(string, width) def on_heredoc_end(value) heredoc = @heredocs[-1] - @heredocs[-1] = - Heredoc.new( - beginning: heredoc.beginning, - ending: value.chomp, - parts: heredoc.parts, - location: - Location.new( - start_line: heredoc.location.start_line, - start_char: heredoc.location.start_char, - end_line: lineno, - end_char: char_pos - ) - ) + @heredocs[-1] = Heredoc.new( + beginning: heredoc.beginning, + ending: value.chomp, + parts: heredoc.parts, + location: + Location.new( + start_line: heredoc.location.start_line, + start_char: heredoc.location.start_char, + end_line: lineno, + end_char: char_pos + ) + ) end # HshPtn represents matching against a hash pattern using the Ruby 2.7+ @@ -4223,11 +6207,55 @@ def on_heredoc_end(value) # end # class HshPtn + class KeywordFormatter + # [Label] the keyword being used + attr_reader :key + + # [untyped] the optional value for the keyword + attr_reader :value + + def initialize(key, value) + @key = key + @value = value + end + + def comments + [] + end + + def format(q) + q.format(key) + + if value + q.text(" ") + q.format(value) + end + end + end + + class KeywordRestFormatter + # [VarField] the parameter that matches the remaining keywords + attr_reader :keyword_rest + + def initialize(keyword_rest) + @keyword_rest = keyword_rest + end + + def comments + [] + end + + def format(q) + q.text("**") + q.format(keyword_rest) + end + end + # [nil | untyped] the optional constant wrapper attr_reader :constant - # [Array[[Label, untyped]]] the set of tuples representing the keywords that - # should be matched against in the pattern + # [Array[ [Label, untyped] ]] the set of tuples representing the keywords + # that should be matched against in the pattern attr_reader :keywords # [nil | VarField] an optional parameter to gather up all remaining keywords @@ -4236,16 +6264,47 @@ class HshPtn # [Location] the location of this node attr_reader :location - def initialize(constant:, keywords:, keyword_rest:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, keywords:, keyword_rest:, location:, comments: []) @constant = constant @keywords = keywords @keyword_rest = keyword_rest @location = location + @comments = comments + end + + def child_nodes + [constant, *keywords.flatten(1), keyword_rest] + end + + def format(q) + parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) } + parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest + contents = -> { q.seplist(parts) { |part| q.format(part) } } + + if constant + q.format(constant) + q.text("[") + contents.call + q.text("]") + return + end + + parent = q.parent + if PATTERNS.any? { |pattern| parent.is_a?(pattern) } + q.text("{ ") + contents.call + q.text(" }") + else + contents.call + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('hshptn') + q.group(2, "(", ")") do + q.text("hshptn") if constant q.breakable @@ -4254,8 +6313,17 @@ def pretty_print(q) if keywords.any? q.breakable - q.group(2, '(', ')') do - q.seplist(keywords) { |keyword| q.pp(keyword) } + q.group(2, "(", ")") do + q.seplist(keywords) do |(key, value)| + q.group(2, "(", ")") do + q.pp(key) + + if value + q.breakable + q.pp(value) + end + end + end end end @@ -4263,6 +6331,8 @@ def pretty_print(q) q.breakable q.pp(keyword_rest) end + + q.pp(Comment::List.new(comments)) end end @@ -4272,11 +6342,16 @@ def to_json(*opts) constant: constant, keywords: keywords, kwrest: keyword_rest, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end + # The list of nodes that represent patterns inside of pattern matching so that + # when a pattern is being printed it knows if it's nested. + PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign] + # :call-seq: # on_hshptn: ( # (nil | untyped) constant, @@ -4306,24 +6381,40 @@ class Ident # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('ident') + q.group(2, "(", ")") do + q.text("ident") + q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) { - type: :@ident, - value: value.force_encoding('UTF-8'), - loc: location + type: :ident, + value: value.force_encoding("UTF-8"), + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4331,14 +6422,59 @@ def to_json(*opts) # :call-seq: # on_ident: (String value) -> Ident def on_ident(value) - node = - Ident.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) + Ident.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end - tokens << node - node + # Formats an If or Unless node. + class ConditionalFormatter + # [String] the keyword associated with this conditional + attr_reader :keyword + + # [If | Unless] the node that is being formatted + attr_reader :node + + def initialize(keyword, node) + @keyword = keyword + @node = node + end + + def format(q) + statements = node.statements + break_format = ->(force:) do + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } + + unless statements.empty? + q.indent do + q.breakable(force: force) + q.format(statements) + end + end + + if node.consequent + q.breakable(force: force) + q.format(node.consequent) + end + + q.breakable(force: force) + q.text("end") + end + + if node.consequent || statements.empty? + q.group { break_format.call(force: true) } + else + q.group do + q.if_break { break_format.call(force: false) }.if_flat do + q.format(node.statements) + q.text(" #{keyword} ") + q.format(node.predicate) + end + end + end + end end # If represents the first clause in an +if+ chain. @@ -4359,16 +6495,34 @@ class If # [Location] the location of this node attr_reader :location - def initialize(predicate:, statements:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + predicate:, + statements:, + consequent:, + location:, + comments: [] + ) @predicate = predicate @statements = statements @consequent = consequent @location = location + @comments = comments + end + + def child_nodes + [predicate, statements, consequent] + end + + def format(q) + ConditionalFormatter.new("if", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('if') + q.group(2, "(", ")") do + q.text("if") q.breakable q.pp(predicate) @@ -4380,6 +6534,8 @@ def pretty_print(q) q.breakable q.pp(consequent) end + + q.pp(Comment::List.new(comments)) end end @@ -4389,7 +6545,8 @@ def to_json(*opts) pred: predicate, stmts: statements, cons: consequent, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4401,8 +6558,8 @@ def to_json(*opts) # (nil | Elsif | Else) consequent # ) -> If def on_if(predicate, statements, consequent) - beginning = find_token(Kw, 'if') - ending = consequent || find_token(Kw, 'end') + beginning = find_token(Kw, "if") + ending = consequent || find_token(Kw, "end") statements.bind(predicate.location.end_char, ending.location.start_char) @@ -4431,16 +6588,59 @@ class IfOp # [Location] the location of this node attr_reader :location - def initialize(predicate:, truthy:, falsy:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(predicate:, truthy:, falsy:, location:, comments: []) @predicate = predicate @truthy = truthy @falsy = falsy @location = location + @comments = comments + end + + def child_nodes + [predicate, truthy, falsy] + end + + def format(q) + q.group do + q.if_break do + q.text("if ") + q.nest("if ".length) { q.format(predicate) } + + q.indent do + q.breakable + q.format(truthy) + end + + q.breakable + q.text("else") + + q.indent do + q.breakable + q.format(falsy) + end + + q.breakable + q.text("end") + end.if_flat do + q.format(predicate) + q.text(" ?") + + q.breakable + q.format(truthy) + q.text(" :") + + q.breakable + q.format(falsy) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('ifop') + q.group(2, "(", ")") do + q.text("ifop") q.breakable q.pp(predicate) @@ -4450,6 +6650,8 @@ def pretty_print(q) q.breakable q.pp(falsy) + + q.pp(Comment::List.new(comments)) end end @@ -4459,7 +6661,8 @@ def to_json(*opts) pred: predicate, tthy: truthy, flsy: falsy, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4475,6 +6678,39 @@ def on_ifop(predicate, truthy, falsy) ) end + # Formats an IfMod or UnlessMod node. + class ConditionalModFormatter + # [String] the keyword associated with this conditional + attr_reader :keyword + + # [IfMod | UnlessMod] the node that is being formatted + attr_reader :node + + def initialize(keyword, node) + @keyword = keyword + @node = node + end + + def format(q) + q.group do + q.if_break do + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } + q.indent do + q.breakable + q.format(node.statement) + end + q.breakable + q.text("end") + end.if_flat do + q.format(node.statement) + q.text(" #{keyword} ") + q.format(node.predicate) + end + end + end + end + # IfMod represents the modifier form of an +if+ statement. # # expression if predicate @@ -4489,21 +6725,35 @@ class IfMod # [Location] the location of this node attr_reader :location - def initialize(statement:, predicate:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) @statement = statement @predicate = predicate @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + def format(q) + ConditionalModFormatter.new("if", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('if_mod') + q.group(2, "(", ")") do + q.text("if_mod") q.breakable q.pp(statement) q.breakable q.pp(predicate) + + q.pp(Comment::List.new(comments)) end end @@ -4512,7 +6762,8 @@ def to_json(*opts) type: :if_mod, stmt: statement, pred: predicate, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4520,7 +6771,7 @@ def to_json(*opts) # :call-seq: # on_if_mod: (untyped predicate, untyped statement) -> IfMod def on_if_mod(predicate, statement) - find_token(Kw, 'if') + find_token(Kw, "if") IfMod.new( statement: statement, @@ -4548,36 +6799,48 @@ class Imaginary # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('imaginary') + q.group(2, "(", ")") do + q.text("imaginary") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@imaginary, value: value, loc: location }.to_json(*opts) + { type: :imaginary, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_imaginary: (String value) -> Imaginary def on_imaginary(value) - node = - Imaginary.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + Imaginary.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # In represents using the +in+ keyword within the Ruby 2.7+ pattern matching @@ -4600,16 +6863,45 @@ class In # [Location] the location of this node attr_reader :location - def initialize(pattern:, statements:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(pattern:, statements:, consequent:, location:, comments: []) @pattern = pattern @statements = statements @consequent = consequent @location = location + @comments = comments + end + + def child_nodes + [pattern, statements, consequent] + end + + def format(q) + keyword = "in " + + q.group do + q.text(keyword) + q.nest(keyword.length) { q.format(pattern) } + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.breakable(force: true) + q.format(consequent) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('in') + q.group(2, "(", ")") do + q.text("in") q.breakable q.pp(pattern) @@ -4621,6 +6913,8 @@ def pretty_print(q) q.breakable q.pp(consequent) end + + q.pp(Comment::List.new(comments)) end end @@ -4630,7 +6924,8 @@ def to_json(*opts) pattern: pattern, stmts: statements, cons: consequent, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4646,10 +6941,13 @@ def on_in(pattern, statements, consequent) # Here we have a rightward assignment return pattern unless statements - beginning = find_token(Kw, 'in') - ending = consequent || find_token(Kw, 'end') + beginning = find_token(Kw, "in") + ending = consequent || find_token(Kw, "end") - statements.bind(beginning.location.end_char, ending.location.start_char) + statements.bind( + find_next_statement_start(pattern.location.end_char), + ending.location.start_char + ) In.new( pattern: pattern, @@ -4670,36 +6968,54 @@ class Int # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + if !value.start_with?("0") && value.length >= 5 && !value.include?("_") + # If it's a plain integer and it doesn't have any underscores separating + # the values, then we're going to insert them every 3 characters + # starting from the right. + index = (value.length + 2) % 3 + q.text(" #{value}"[index..-1].scan(/.../).join("_").strip) + else + q.text(value) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('int') + q.group(2, "(", ")") do + q.text("int") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@int, value: value, loc: location }.to_json(*opts) + { type: :int, value: value, loc: location, cmts: comments }.to_json(*opts) end end # :call-seq: # on_int: (String value) -> Int def on_int(value) - node = - Int.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + Int.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # IVar represents an instance variable literal. @@ -4713,36 +7029,48 @@ class IVar # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('ivar') + q.group(2, "(", ")") do + q.text("ivar") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@ivar, value: value, loc: location }.to_json(*opts) + { type: :ivar, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_ivar: (String value) -> IVar def on_ivar(value) - node = - IVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + IVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # Kw represents the use of a keyword. It can be almost anywhere in the syntax @@ -4765,22 +7093,36 @@ class Kw # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('kw') + q.group(2, "(", ")") do + q.text("kw") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@kw, value: value, loc: location }.to_json(*opts) + { type: :kw, value: value, loc: location, cmts: comments }.to_json(*opts) end end @@ -4809,29 +7151,49 @@ class KwRestParam # [Location] the location of this node attr_reader :location - def initialize(name:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, location:, comments: []) @name = name @location = location + @comments = comments + end + + def child_nodes + [name] + end + + def format(q) + q.text("**") + q.format(name) if name end def pretty_print(q) - q.group(2, '(', ')') do - q.text('kwrest_param') + q.group(2, "(", ")") do + q.text("kwrest_param") q.breakable q.pp(name) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :kwrest_param, name: name, loc: location }.to_json(*opts) + { + type: :kwrest_param, + name: name, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_kwrest_param: ((nil | Ident) name) -> KwRestParam def on_kwrest_param(name) - location = find_token(Op, '**').location + location = find_token(Op, "**").location location = location.to(name.location) if name KwRestParam.new(name: name, location: location) @@ -4857,37 +7219,49 @@ class Label # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('label') + q.group(2, "(", ")") do + q.text("label") q.breakable - q.text(':') + q.text(":") q.text(value[0...-1]) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@label, value: value, loc: location }.to_json(*opts) + { type: :label, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_label: (String value) -> Label def on_label(value) - node = - Label.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + Label.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # LabelEnd represents the end of a dynamic symbol. @@ -4908,19 +7282,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('label_end') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@label_end, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -4950,21 +7311,64 @@ class Lambda # [Location] the location of this node attr_reader :location - def initialize(params:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(params:, statements:, location:, comments: []) @params = params @statements = statements @location = location + @comments = comments + end + + def child_nodes + [params, statements] + end + + def format(q) + q.group(0, "->") do + if params.is_a?(Paren) + q.format(params) unless params.contents.empty? + elsif !params.empty? + q.text("(") + q.format(params) + q.text(")") + end + + q.text(" ") + q.if_break do + force_parens = + q.parents.any? do |node| + node.is_a?(Command) || node.is_a?(CommandCall) + end + + q.text(force_parens ? "{" : "do") + q.indent do + q.breakable + q.format(statements) + end + + q.breakable + q.text(force_parens ? "}" : "end") + end.if_flat do + q.text("{ ") + q.format(statements) + q.text(" }") + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('lambda') + q.group(2, "(", ")") do + q.text("lambda") q.breakable q.pp(params) q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -4973,7 +7377,8 @@ def to_json(*opts) type: :lambda, params: params, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -4990,8 +7395,8 @@ def on_lambda(params, statements) opening = tokens.delete(token) closing = find_token(RBrace) else - opening = find_token(Kw, 'do') - closing = find_token(Kw, 'end') + opening = find_token(Kw, "do") + closing = find_token(Kw, "end") end statements.bind(opening.location.end_char, closing.location.start_char) @@ -5011,22 +7416,38 @@ class LBrace # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('lbrace') + q.group(2, "(", ")") do + q.text("lbrace") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@lbrace, value: value, loc: location }.to_json(*opts) + { type: :lbrace, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -5055,19 +7476,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('lbracket') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@lbracket, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -5091,22 +7499,38 @@ class LParen # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('lparen') + q.group(2, "(", ")") do + q.text("lparen") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@lparen, value: value, loc: location }.to_json(*opts) + { type: :lparen, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -5142,8 +7566,7 @@ def on_lparen(value) # first, = value # class MAssign - # [Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen] the target of the multiple - # assignment + # [MLHS | MLHSParen] the target of the multiple assignment attr_reader :target # [untyped] the value being assigned @@ -5152,37 +7575,65 @@ class MAssign # [Location] the location of this node attr_reader :location - def initialize(target:, value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, value:, location:, comments: []) @target = target @value = value @location = location + @comments = comments + end + + def child_nodes + [target, value] + end + + def format(q) + q.group do + q.group do + q.format(target) + q.text(",") if target.is_a?(MLHS) && target.comma + end + + q.text(" =") + q.indent do + q.breakable + q.format(value) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('massign') + q.group(2, "(", ")") do + q.text("massign") q.breakable q.pp(target) q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :massign, target: target, value: value, loc: location }.to_json(*opts) + { + type: :massign, + target: target, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: - # on_massign: ( - # (Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen) target, - # untyped value - # ) -> MAssign + # on_massign: ((MLHS | MLHSParen) target, untyped value) -> MAssign def on_massign(target, value) comma_range = target.location.end_char...value.location.start_char - target.comma = true if source[comma_range].strip.start_with?(',') + target.comma = true if source[comma_range].strip.start_with?(",") MAssign.new( target: target, @@ -5210,27 +7661,43 @@ class MethodAddArg # [Call | FCall] the method call attr_reader :call - # [ArgParen | Args | ArgsAddBlock] the arguments to the method call + # [ArgParen | Args] the arguments to the method call attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(call:, arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(call:, arguments:, location:, comments: []) @call = call @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [call, arguments] + end + + def format(q) + q.format(call) + q.text(" ") if !arguments.is_a?(ArgParen) && arguments.parts.any? + q.format(arguments) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('method_add_arg') + q.group(2, "(", ")") do + q.text("method_add_arg") q.breakable q.pp(call) q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end @@ -5239,7 +7706,8 @@ def to_json(*opts) type: :method_add_arg, call: call, args: arguments, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -5247,11 +7715,10 @@ def to_json(*opts) # :call-seq: # on_method_add_arg: ( # (Call | FCall) call, - # (ArgParen | Args | ArgsAddBlock) arguments + # (ArgParen | Args) arguments # ) -> MethodAddArg def on_method_add_arg(call, arguments) location = call.location - location = location.to(arguments.location) unless arguments.is_a?(Args) MethodAddArg.new(call: call, arguments: arguments, location: location) @@ -5271,21 +7738,36 @@ class MethodAddBlock # [Location] the location of this node attr_reader :location - def initialize(call:, block:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(call:, block:, location:, comments: []) @call = call @block = block @location = location + @comments = comments + end + + def child_nodes + [call, block] + end + + def format(q) + q.format(call) + q.format(block) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('method_add_block') + q.group(2, "(", ")") do + q.text("method_add_block") q.breakable q.pp(call) q.breakable q.pp(block) + + q.pp(Comment::List.new(comments)) end end @@ -5294,7 +7776,8 @@ def to_json(*opts) type: :method_add_block, call: call, block: block, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -5318,159 +7801,94 @@ def on_method_add_block(call, block) # first, second, third = value # class MLHS - # Array[ArefField | Field | Identifier | MlhsParen | VarField] the parts of - # the left-hand side of a multiple assignment + # Array[ARefField | ArgStar | Field | Ident | MLHSParen | VarField] the + # parts of the left-hand side of a multiple assignment attr_reader :parts # [boolean] whether or not there is a trailing comma at the end of this # list, which impacts destructuring. It's an attr_accessor so that while # the syntax tree is being built it can be set by its parent node attr_accessor :comma + alias comma? comma # [Location] the location of this node attr_reader :location - def initialize(parts:, comma: false, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, comma: false, location:, comments: []) @parts = parts @comma = comma @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + q.seplist(parts) { |part| q.format(part) } end def pretty_print(q) - q.group(2, '(', ')') do - q.text('mlhs') + q.group(2, "(", ")") do + q.text("mlhs") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :mlhs, parts: parts, comma: comma, loc: location }.to_json(*opts) + { + type: :mlhs, + parts: parts, + comma: comma, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_mlhs_add: ( # MLHS mlhs, - # (ArefField | Field | Identifier | MlhsParen | VarField) part + # (ARefField | Field | Ident | MLHSParen | VarField) part # ) -> MLHS def on_mlhs_add(mlhs, part) - if mlhs.parts.empty? - MLHS.new(parts: [part], location: part.location) - else - MLHS.new( - parts: mlhs.parts << part, - location: mlhs.location.to(part.location) - ) - end - end - - # MLHSAddPost represents adding another set of variables onto a list of - # assignments after a splat variable within a multiple assignment. - # - # left, *middle, right = values - # - class MLHSAddPost - # [MlhsAddStar] the value being starred - attr_reader :star - - # [Mlhs] the values after the star - attr_reader :mlhs - - # [Location] the location of this node - attr_reader :location - - def initialize(star:, mlhs:, location:) - @star = star - @mlhs = mlhs - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('mlhs_add_post') - - q.breakable - q.pp(star) - - q.breakable - q.pp(mlhs) - end - end + location = + mlhs.parts.empty? ? part.location : mlhs.location.to(part.location) - def to_json(*opts) - { type: :mlhs_add_post, star: star, mlhs: mlhs, loc: location }.to_json( - *opts - ) - end + MLHS.new(parts: mlhs.parts << part, location: location) end # :call-seq: - # on_mlhs_add_post: (MLHSAddStar star, MLHS mlhs) -> MLHSAddPost - def on_mlhs_add_post(star, mlhs) - MLHSAddPost.new( - star: star, - mlhs: mlhs, - location: star.location.to(mlhs.location) + # on_mlhs_add_post: (MLHS left, MLHS right) -> MLHS + def on_mlhs_add_post(left, right) + MLHS.new( + parts: left.parts + right.parts, + location: left.location.to(right.location) ) end - # MLHSAddStar represents a splatted variable inside of a multiple assignment - # on the left hand side. - # - # first, *rest = values - # - class MLHSAddStar - # [MLHS] the values before the starred expression - attr_reader :mlhs - - # [nil | ArefField | Field | Identifier | VarField] the expression being - # splatted - attr_reader :star - - # [Location] the location of this node - attr_reader :location - - def initialize(mlhs:, star:, location:) - @mlhs = mlhs - @star = star - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('mlhs_add_star') - - q.breakable - q.pp(mlhs) - - q.breakable - q.pp(star) - end - end - - def to_json(*opts) - { type: :mlhs_add_star, mlhs: mlhs, star: star, loc: location }.to_json( - *opts - ) - end - end - # :call-seq: # on_mlhs_add_star: ( # MLHS mlhs, - # (nil | ArefField | Field | Identifier | VarField) part - # ) -> MLHSAddStar + # (nil | ARefField | Field | Ident | VarField) part + # ) -> MLHS def on_mlhs_add_star(mlhs, part) - beginning = find_token(Op, '*') + beginning = find_token(Op, "*") ending = part || beginning - MLHSAddStar.new( - mlhs: mlhs, - star: part, - location: beginning.location.to(ending.location) - ) + location = beginning.location.to(ending.location) + arg_star = ArgStar.new(value: part, location: location) + + location = mlhs.location.to(location) unless mlhs.parts.empty? + MLHS.new(parts: mlhs.parts << arg_star, location: location) end # :call-seq: @@ -5485,42 +7903,72 @@ def on_mlhs_new # (left, right) = value # class MLHSParen - # [Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen] the contents inside of the - # parentheses + # [MLHS | MLHSParen] the contents inside of the parentheses attr_reader :contents # [Location] the location of this node attr_reader :location - def initialize(contents:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(contents:, location:, comments: []) @contents = contents @location = location + @comments = comments + end + + def child_nodes + [contents] + end + + def format(q) + parent = q.parent + + if parent.is_a?(MAssign) || parent.is_a?(MLHSParen) + q.format(contents) + else + q.group(0, "(", ")") do + q.indent do + q.breakable("") + q.format(contents) + q.text(",") if contents.is_a?(MLHS) && contents.comma? + end + + q.breakable("") + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('mlhs_paren') + q.group(2, "(", ")") do + q.text("mlhs_paren") q.breakable q.pp(contents) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :mlhs_paren, cnts: contents, loc: location }.to_json(*opts) + { + type: :mlhs_paren, + cnts: contents, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: - # on_mlhs_paren: ( - # (Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen) contents - # ) -> MLHSParen + # on_mlhs_paren: ((MLHS | MLHSParen) contents) -> MLHSParen def on_mlhs_paren(contents) lparen = find_token(LParen) rparen = find_token(RParen) comma_range = lparen.location.end_char...rparen.location.start_char - contents.comma = true if source[comma_range].strip.end_with?(',') + contents.comma = true if source[comma_range].strip.end_with?(",") MLHSParen.new( contents: contents, @@ -5543,21 +7991,60 @@ class ModuleDeclaration # [Location] the location of this node attr_reader :location - def initialize(constant:, bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, bodystmt:, location:, comments: []) @constant = constant @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [constant, bodystmt] + end + + def format(q) + declaration = -> do + q.group do + q.text("module ") + q.format(constant) + end + end + + if bodystmt.empty? + q.group do + declaration.call + q.breakable(force: true) + q.text("end") + end + else + q.group do + declaration.call + + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + + q.breakable(force: true) + q.text("end") + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('module') + q.group(2, "(", ")") do + q.text("module") q.breakable q.pp(constant) q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end @@ -5566,7 +8053,8 @@ def to_json(*opts) type: :module, constant: constant, bodystmt: bodystmt, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -5577,8 +8065,8 @@ def to_json(*opts) # BodyStmt bodystmt # ) -> ModuleDeclaration def on_module(constant, bodystmt) - beginning = find_token(Kw, 'module') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "module") + ending = find_token(Kw, "end") bodystmt.bind( find_next_statement_start(constant.location.end_char), @@ -5604,22 +8092,38 @@ class MRHS # [Location] the location of this node attr_reader :location - def initialize(parts:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) @parts = parts @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + q.seplist(parts) { |part| q.format(part) } end def pretty_print(q) - q.group(2, '(', ')') do - q.text('mrhs') + q.group(2, "(", ")") do + q.text("mrhs") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :mrhs, parts: parts, loc: location }.to_json(*opts) + { type: :mrhs, parts: parts, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -5632,112 +8136,42 @@ def on_mrhs_new # :call-seq: # on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS def on_mrhs_add(mrhs, part) - if mrhs.is_a?(MRHSNewFromArgs) - MRHS.new( - parts: [*mrhs.arguments.parts, part], - location: mrhs.location.to(part.location) - ) - elsif mrhs.parts.empty? - MRHS.new(parts: [part], location: mrhs.location) - else - MRHS.new(parts: mrhs.parts << part, loc: mrhs.location.to(part.location)) - end - end - - # MRHSAddStar represents using the splat operator to expand out a value on the - # right hand side of a multiple assignment. - # - # values = first, *rest - # - class MRHSAddStar - # [MRHS | MRHSNewFromArgs] the values before the splatted expression - attr_reader :mrhs - - # [untyped] the splatted expression - attr_reader :star - - # [Location] the location of this node - attr_reader :location - - def initialize(mrhs:, star:, location:) - @mrhs = mrhs - @star = star - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('mrhs_add_star') - - q.breakable - q.pp(mrhs) - - q.breakable - q.pp(star) + location = + if mrhs.parts.empty? + mrhs.location + else + mrhs.location.to(part.location) end - end - def to_json(*opts) - { type: :mrhs_add_star, mrhs: mrhs, star: star, loc: location }.to_json( - *opts - ) - end + MRHS.new(parts: mrhs.parts << part, location: location) end # :call-seq: - # on_mrhs_add_star: ( - # (MRHS | MRHSNewFromArgs) mrhs, - # untyped star - # ) -> MRHSAddStar - def on_mrhs_add_star(mrhs, star) - beginning = find_token(Op, '*') - ending = star || beginning - - MRHSAddStar.new( - mrhs: mrhs, - star: star, - location: beginning.location.to(ending.location) - ) - end - - # MRHSNewFromArgs represents the shorthand of a multiple assignment that - # allows you to assign values using just commas as opposed to assigning from - # an array. - # - # values = first, second, third - # - class MRHSNewFromArgs - # [Args | ArgsAddStar] the arguments being used in the assignment - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - def initialize(arguments:, location:) - @arguments = arguments - @location = location - end + # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS + def on_mrhs_add_star(mrhs, value) + beginning = find_token(Op, "*") + ending = value || beginning - def pretty_print(q) - q.group(2, '(', ')') do - q.text('mrhs_new_from_args') + arg_star = + ArgStar.new( + value: value, + location: beginning.location.to(ending.location) + ) - q.breakable - q.pp(arguments) + location = + if mrhs.parts.empty? + arg_star.location + else + mrhs.location.to(arg_star.location) end - end - def to_json(*opts) - { type: :mrhs_new_from_args, args: arguments, loc: location }.to_json( - *opts - ) - end + MRHS.new(parts: mrhs.parts << arg_star, location: location) end # :call-seq: - # on_mrhs_new_from_args: ((Args | ArgsAddStar) arguments) -> MRHSNewFromArgs + # on_mrhs_new_from_args: (Args arguments) -> MRHS def on_mrhs_new_from_args(arguments) - MRHSNewFromArgs.new(arguments: arguments, location: arguments.location) + MRHS.new(parts: arguments.parts, location: arguments.location) end # Next represents using the +next+ keyword. @@ -5758,38 +8192,54 @@ def on_mrhs_new_from_args(arguments) # next(value) # class Next - # [Args | ArgsAddBlock] the arguments passed to the next keyword + # [Args] the arguments passed to the next keyword attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + def format(q) + FlowControlFormatter.new("next", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('next') + q.group(2, "(", ")") do + q.text("next") q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :next, args: arguments, loc: location }.to_json(*opts) + { type: :next, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_next: ((Args | ArgsAddBlock) arguments) -> Next + # on_next: (Args arguments) -> Next def on_next(arguments) - keyword = find_token(Kw, 'next') + keyword = find_token(Kw, "next") location = keyword.location - location = location.to(arguments.location) unless arguments.is_a?(Args) + location = location.to(arguments.location) if arguments.parts.any? Next.new(arguments: arguments, location: location) end @@ -5814,22 +8264,36 @@ class Op # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('op') + q.group(2, "(", ")") do + q.text("op") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@op, value: value, loc: location }.to_json(*opts) + { type: :op, value: value, loc: location, cmts: comments }.to_json(*opts) end end @@ -5852,7 +8316,7 @@ def on_op(value) # variable += value # class OpAssign - # [ArefField | ConstPathField | Field | TopConstField | VarField] the target + # [ARefField | ConstPathField | Field | TopConstField | VarField] the target # to assign the result of the expression to attr_reader :target @@ -5865,16 +8329,36 @@ class OpAssign # [Location] the location of this node attr_reader :location - def initialize(target:, operator:, value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, operator:, value:, location:, comments: []) @target = target @operator = operator @value = value @location = location + @comments = comments + end + + def child_nodes + [target, operator, value] + end + + def format(q) + q.group do + q.format(target) + q.text(" ") + q.format(operator) + q.indent do + q.breakable + q.format(value) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('opassign') + q.group(2, "(", ")") do + q.text("opassign") q.breakable q.pp(target) @@ -5884,6 +8368,8 @@ def pretty_print(q) q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end @@ -5893,14 +8379,15 @@ def to_json(*opts) target: target, op: operator, value: value, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end # :call-seq: # on_opassign: ( - # (ArefField | ConstPathField | Field | TopConstField | VarField) target, + # (ARefField | ConstPathField | Field | TopConstField | VarField) target, # Op operator, # untyped value # ) -> OpAssign @@ -5922,21 +8409,93 @@ def on_opassign(target, operator, value) # def method(param) end # class Params - # [Array[Ident]] any required parameters + class OptionalFormatter + # [Ident] the name of the parameter + attr_reader :name + + # [untyped] the value of the parameter + attr_reader :value + + def initialize(name, value) + @name = name + @value = value + end + + def comments + [] + end + + def format(q) + q.format(name) + q.text(" = ") + q.format(value) + end + end + + class KeywordFormatter + # [Ident] the name of the parameter + attr_reader :name + + # [nil | untyped] the value of the parameter + attr_reader :value + + def initialize(name, value) + @name = name + @value = value + end + + def comments + [] + end + + def format(q) + q.format(name) + + if value + q.text(" ") + q.format(value) + end + end + end + + class KeywordRestFormatter + # [:nil | KwRestParam] the value of the parameter + attr_reader :value + + def initialize(value) + @value = value + end + + def comments + [] + end + + def format(q) + if value == :nil + q.text("**nil") + else + q.format(value) + end + end + end + + # [Array[ Ident ]] any required parameters attr_reader :requireds - # [Array[[Ident, untyped]]] any optional parameters and their default values + # [Array[ [ Ident, untyped ] ]] any optional parameters and their default + # values attr_reader :optionals # [nil | ArgsForward | ExcessedComma | RestParam] the optional rest # parameter attr_reader :rest - # Array[Ident] any positional parameters that exist after a rest parameter + # [Array[ Ident ]] any positional parameters that exist after a rest + # parameter attr_reader :posts - # Array[[Ident, nil | untyped]] any keyword parameters and their optional - # default values + # [Array[ [ Ident, nil | untyped ] ]] any keyword parameters and their + # optional default values attr_reader :keywords # [nil | :nil | KwRestParam] the optional keyword rest parameter @@ -5948,6 +8507,9 @@ class Params # [Location] the location of this node attr_reader :location + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + def initialize( requireds: [], optionals: [], @@ -5956,7 +8518,8 @@ def initialize( keywords: [], keyword_rest: nil, block: nil, - location: + location:, + comments: [] ) @requireds = requireds @optionals = optionals @@ -5966,6 +8529,7 @@ def initialize( @keyword_rest = keyword_rest @block = block @location = location + @comments = comments end # Params nodes are the most complicated in the tree. Occasionally you want @@ -5974,26 +8538,62 @@ def initialize( # it's missing. def empty? requireds.empty? && optionals.empty? && !rest && posts.empty? && - keywords.empty? && !keyword_rest && !block + keywords.empty? && + !keyword_rest && + !block + end + + def child_nodes + [ + *requireds, + *optionals.flatten(1), + rest, + *posts, + *keywords.flatten(1), + (keyword_rest if keyword_rest != :nil), + block + ] + end + + def format(q) + parts = [ + *requireds, + *optionals.map { |(name, value)| OptionalFormatter.new(name, value) } + ] + + parts << rest if rest && !rest.is_a?(ExcessedComma) + parts += + [ + *posts, + *keywords.map { |(name, value)| KeywordFormatter.new(name, value) } + ] + + parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest + parts << block if block + + q.nest(0) do + q.seplist(parts) { |part| q.format(part) } + q.format(rest) if rest && rest.is_a?(ExcessedComma) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('params') + q.group(2, "(", ")") do + q.text("params") if requireds.any? q.breakable - q.group(2, '(', ')') { q.seplist(requireds) { |name| q.pp(name) } } + q.group(2, "(", ")") { q.seplist(requireds) { |name| q.pp(name) } } end if optionals.any? q.breakable - q.group(2, '(', ')') do + q.group(2, "(", ")") do q.seplist(optionals) do |(name, default)| q.pp(name) - q.text('=') + q.text("=") q.group(2) do - q.breakable('') + q.breakable("") q.pp(default) end end @@ -6007,19 +8607,19 @@ def pretty_print(q) if posts.any? q.breakable - q.group(2, '(', ')') { q.seplist(posts) { |value| q.pp(value) } } + q.group(2, "(", ")") { q.seplist(posts) { |value| q.pp(value) } } end if keywords.any? q.breakable - q.group(2, '(', ')') do + q.group(2, "(", ")") do q.seplist(keywords) do |(name, default)| q.pp(name) if default - q.text('=') + q.text("=") q.group(2) do - q.breakable('') + q.breakable("") q.pp(default) end end @@ -6036,6 +8636,8 @@ def pretty_print(q) q.breakable q.pp(block) end + + q.pp(Comment::List.new(comments)) end end @@ -6049,7 +8651,8 @@ def to_json(*opts) keywords: keywords, kwrest: keyword_rest, block: block, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -6073,15 +8676,16 @@ def on_params( keyword_rest, block ) - parts = [ - *requireds, - *optionals&.flatten(1), - rest, - *posts, - *keywords&.flat_map { |(key, value)| [key, value || nil] }, - (keyword_rest if keyword_rest != :nil), - block - ].compact + parts = + [ + *requireds, + *optionals&.flatten(1), + rest, + *posts, + *keywords&.flat_map { |(key, value)| [key, value || nil] }, + (keyword_rest if keyword_rest != :nil), + block + ].compact location = if parts.any? @@ -6118,25 +8722,55 @@ class Paren # [Location] the location of this node attr_reader :location - def initialize(lparen:, contents:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lparen:, contents:, location:, comments: []) @lparen = lparen @contents = contents @location = location + @comments = comments + end + + def child_nodes + [lparen, contents] + end + + def format(q) + q.group do + q.format(lparen) + + if !contents.is_a?(Params) || !contents.empty? + q.indent do + q.breakable("") + q.format(contents) + end + end + + q.breakable("") + q.text(")") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('paren') + q.group(2, "(", ")") do + q.text("paren") q.breakable q.pp(contents) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :paren, lparen: lparen, cnts: contents, loc: location }.to_json( - *opts - ) + { + type: :paren, + lparen: lparen, + cnts: contents, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -6195,22 +8829,38 @@ class Period # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('period') + q.group(2, "(", ")") do + q.text("period") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@period, value: value, loc: location }.to_json(*opts) + { type: :period, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -6228,24 +8878,35 @@ class Program # [Statements] the top-level expressions of the program attr_reader :statements - # [Array[Comment | EmbDoc]] the comments inside the program - attr_reader :comments - # [Location] the location of this node attr_reader :location - def initialize(statements:, comments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statements:, location:, comments: []) @statements = statements - @comments = comments @location = location + @comments = comments + end + + def child_nodes + [statements] + end + + def format(q) + q.format(statements) + q.breakable(force: true) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('program') + q.group(2, "(", ")") do + q.text("program") q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -6254,7 +8915,8 @@ def to_json(*opts) type: :program, stmts: statements, comments: comments, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -6273,7 +8935,99 @@ def on_program(statements) statements.body << @__end__ if @__end__ statements.bind(0, source.length) - Program.new(statements: statements, comments: @comments, location: location) + program = Program.new(statements: statements, location: location) + attach_comments(program, @comments) + + program + end + + # Attaches comments to the nodes in the tree that most closely correspond to + # the location of the comments. + def attach_comments(program, comments) + comments.each do |comment| + preceding, enclosing, following = nearest_nodes(program, comment) + + if comment.inline? + if preceding + preceding.comments << comment + comment.trailing! + elsif following + following.comments << comment + comment.leading! + elsif enclosing + enclosing.comments << comment + else + program.comments << comment + end + else + # If a comment exists on its own line, prefer a leading comment. + if following + following.comments << comment + comment.leading! + elsif preceding + preceding.comments << comment + comment.trailing! + elsif enclosing + enclosing.comments << comment + else + program.comments << comment + end + end + end + end + + # Responsible for finding the nearest nodes to the given comment within the + # context of the given encapsulating node. + def nearest_nodes(node, comment) + comment_start = comment.location.start_char + comment_end = comment.location.end_char + + child_nodes = node.child_nodes.compact + preceding = nil + following = nil + + left = 0 + right = child_nodes.length + + # This is a custom binary search that finds the nearest nodes to the given + # comment. When it finds a node that completely encapsulates the comment, it + # recursed downward into the tree. + while left < right + middle = (left + right) / 2 + child = child_nodes[middle] + + node_start = child.location.start_char + node_end = child.location.end_char + + if node_start <= comment_start && comment_end <= node_end + # The comment is completely contained by this child node. Abandon the + # binary search at this level. + return nearest_nodes(child, comment) + end + + if node_end <= comment_start + # This child node falls completely before the comment. Because we will + # never consider this node or any nodes before it again, this node must + # be the closest preceding node we have encountered so far. + preceding = child + left = middle + 1 + next + end + + if comment_end <= node_start + # This child node falls completely after the comment. Because we will + # never consider this node or any nodes after it again, this node must + # be the closest following node we have encountered so far. + following = child + right = middle + next + end + + # This should only happen if there is a bug in this parser. + raise "Comment location overlaps with node location" + end + + [preceding, node, following] end # QSymbols represents a symbol literal array without interpolation. @@ -6281,28 +9035,55 @@ def on_program(statements) # %i[one two three] # class QSymbols - # [Array[TStringContent]] the elements of the array + # [Array[ TStringContent ]] the elements of the array attr_reader :elements # [Location] the location of this node attr_reader :location - def initialize(elements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(elements:, location:, comments: []) @elements = elements @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.group(0, "%i[", "]") do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('qsymbols') + q.group(2, "(", ")") do + q.text("qsymbols") q.breakable - q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } } + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :qsymbols, elems: elements, loc: location }.to_json(*opts) + { + type: :qsymbols, + elems: elements, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -6333,19 +9114,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('qsymbols_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@qsymbols_beg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -6374,28 +9142,52 @@ def on_qsymbols_new # %w[one two three] # class QWords - # [Array[TStringContent]] the elements of the array + # [Array[ TStringContent ]] the elements of the array attr_reader :elements # [Location] the location of this node attr_reader :location - def initialize(elements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(elements:, location:, comments: []) @elements = elements @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.group(0, "%w[", "]") do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('qwords') + q.group(2, "(", ")") do + q.text("qwords") q.breakable - q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } } + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :qwords, elems: elements, loc: location }.to_json(*opts) + { type: :qwords, elems: elements, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -6423,21 +9215,8 @@ class QWordsBeg attr_reader :location def initialize(value:, location:) - @value = value - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('qwords_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@qwords_beg, value: value, loc: location }.to_json(*opts) + @value = value + @location = location end end @@ -6462,47 +9241,59 @@ def on_qwords_new QWords.new(elements: [], location: qwords_beg.location) end - # Rational represents the use of a rational number literal. + # RationalLiteral represents the use of a rational number literal. # # 1r # - class Rational + class RationalLiteral # [String] the rational number literal attr_reader :value # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('rational') + q.group(2, "(", ")") do + q.text("rational") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :@rational, value: value, loc: location }.to_json(*opts) + { type: :rational, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_rational: (String value) -> Rational + # on_rational: (String value) -> RationalLiteral def on_rational(value) - node = - Rational.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node + RationalLiteral.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) end # RBrace represents the use of a right brace, i.e., +++. @@ -6517,19 +9308,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('rbrace') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@rbrace, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -6557,19 +9335,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('rbracket') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@rbracket, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -6596,29 +9361,45 @@ class Redo # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('redo') + q.group(2, "(", ")") do + q.text("redo") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :redo, value: value, loc: location }.to_json(*opts) + { type: :redo, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_redo: () -> Redo def on_redo - keyword = find_token(Kw, 'redo') + keyword = find_token(Kw, "redo") Redo.new(value: keyword.value, location: keyword.location) end @@ -6633,7 +9414,7 @@ class RegexpContent # [String] the opening of the regular expression attr_reader :beginning - # [Array[StringDVar | StringEmbExpr | TStringContent]] the parts of the + # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the # regular expression attr_reader :parts @@ -6645,21 +9426,6 @@ def initialize(beginning:, parts:, location:) @parts = parts @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('regexp') - - q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } - end - end - - def to_json(*opts) - { type: :regexp, beging: beginning, parts: parts, loc: location }.to_json( - *opts - ) - end end # :call-seq: @@ -6695,19 +9461,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('regexp_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@regexp_beg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -6744,19 +9497,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('regexp_end') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@regexp_end, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -6779,26 +9519,55 @@ class RegexpLiteral # [String] the ending of the regular expression literal attr_reader :ending - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the # regular expression literal attr_reader :parts - # [Locatione] the location of this node + # [Location] the location of this node attr_reader :location - def initialize(beginning:, ending:, parts:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, ending:, parts:, location:, comments: []) @beginning = beginning @ending = ending @parts = parts @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + braces = ambiguous?(q) || include?(%r{\/}) + + if braces && include?(/[{}]/) + q.group do + q.text(beginning) + q.format_each(parts) + q.text(ending) + end + else + q.group do + q.text(braces ? "%r{" : "/") + q.format_each(parts) + q.text(braces ? "}" : "/") + q.text(ending[1..-1]) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('regexp_literal') + q.group(2, "(", ")") do + q.text("regexp_literal") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end @@ -6808,9 +9577,30 @@ def to_json(*opts) beging: beginning, ending: ending, parts: parts, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end + + private + + def include?(pattern) + parts.any? do |part| + part.is_a?(TStringContent) && part.value.match?(pattern) + end + end + + # If the first part of this regex is plain string content, we have a space + # or an =, and we're contained within a command or command_call node, then + # we want to use braces because otherwise we could end up with an ambiguous + # operator, e.g. foo / bar/ or foo /=bar/ + def ambiguous?(q) + return false if parts.empty? + part = parts.first + + part.is_a?(TStringContent) && part.value.start_with?(" ", "=") && + q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) } + end end # :call-seq: @@ -6856,21 +9646,45 @@ class RescueEx # [Location] the location of this node attr_reader :location - def initialize(exceptions:, variable:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(exceptions:, variable:, location:, comments: []) @exceptions = exceptions @variable = variable @location = location + @comments = comments + end + + def child_nodes + [*exceptions, variable] + end + + def format(q) + q.group do + if exceptions + q.text(" ") + q.format(exceptions) + end + + if variable + q.text(" => ") + q.format(variable) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('rescue_ex') + q.group(2, "(", ")") do + q.text("rescue_ex") q.breakable q.pp(exceptions) q.breakable q.pp(variable) + + q.pp(Comment::List.new(comments)) end end @@ -6879,7 +9693,8 @@ def to_json(*opts) type: :rescue_ex, extns: exceptions, var: variable, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -6903,11 +9718,21 @@ class Rescue # [Location] the location of this node attr_reader :location - def initialize(exception:, statements:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + exception:, + statements:, + consequent:, + location:, + comments: [] + ) @exception = exception @statements = statements @consequent = consequent @location = location + @comments = comments end def bind_end(end_char) @@ -6927,9 +9752,37 @@ def bind_end(end_char) end end + def child_nodes + [exception, statements, consequent] + end + + def format(q) + q.group do + q.text("rescue") + + if exception + q.nest("rescue ".length) { q.format(exception) } + else + q.text(" StandardError") + end + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.breakable(force: true) + q.format(consequent) + end + end + end + def pretty_print(q) - q.group(2, '(', ')') do - q.text('rescue') + q.group(2, "(", ")") do + q.text("rescue") if exception q.breakable @@ -6943,6 +9796,8 @@ def pretty_print(q) q.breakable q.pp(consequent) end + + q.pp(Comment::List.new(comments)) end end @@ -6952,7 +9807,8 @@ def to_json(*opts) extn: exception, stmts: statements, cons: consequent, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -6965,7 +9821,7 @@ def to_json(*opts) # (nil | Rescue) consequent # ) -> Rescue def on_rescue(exceptions, variable, statements, consequent) - keyword = find_token(Kw, 'rescue') + keyword = find_token(Kw, "rescue") exceptions = exceptions[0] if exceptions.is_a?(Array) last_node = variable || exceptions || keyword @@ -7020,35 +9876,65 @@ class RescueMod # [Location] the location of this node attr_reader :location - def initialize(statement:, value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, value:, location:, comments: []) @statement = statement @value = value @location = location + @comments = comments + end + + def child_nodes + [statement, value] + end + + def format(q) + q.group(0, "begin", "end") do + q.indent do + q.breakable(force: true) + q.format(statement) + end + q.breakable(force: true) + q.text("rescue StandardError") + q.indent do + q.breakable(force: true) + q.format(value) + end + q.breakable(force: true) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('rescue_mod') + q.group(2, "(", ")") do + q.text("rescue_mod") q.breakable q.pp(statement) q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :rescue_mod, stmt: statement, value: value, loc: location }.to_json( - *opts - ) + { + type: :rescue_mod, + stmt: statement, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_rescue_mod: (untyped statement, untyped value) -> RescueMod def on_rescue_mod(statement, value) - find_token(Kw, 'rescue') + find_token(Kw, "rescue") RescueMod.new( statement: statement, @@ -7069,29 +9955,46 @@ class RestParam # [Location] the location of this node attr_reader :location - def initialize(name:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, location:, comments: []) @name = name @location = location + @comments = comments + end + + def child_nodes + [name] + end + + def format(q) + q.text("*") + q.format(name) if name end def pretty_print(q) - q.group(2, '(', ')') do - q.text('rest_param') + q.group(2, "(", ")") do + q.text("rest_param") q.breakable q.pp(name) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :rest_param, name: name, loc: location }.to_json(*opts) + { type: :rest_param, name: name, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_rest_param: ((nil | Ident) name) -> RestParam def on_rest_param(name) - location = find_token(Op, '*').location + location = find_token(Op, "*").location location = location.to(name.location) if name RestParam.new(name: name, location: location) @@ -7108,29 +10011,45 @@ class Retry # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('retry') + q.group(2, "(", ")") do + q.text("retry") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :retry, value: value, loc: location }.to_json(*opts) + { type: :retry, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_retry: () -> Retry def on_retry - keyword = find_token(Kw, 'retry') + keyword = find_token(Kw, "retry") Retry.new(value: keyword.value, location: keyword.location) end @@ -7140,35 +10059,51 @@ def on_retry # return value # class Return - # [Args | ArgsAddBlock] the arguments being passed to the keyword + # [Args] the arguments being passed to the keyword attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + def format(q) + FlowControlFormatter.new("return", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('return') + q.group(2, "(", ")") do + q.text("return") q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :return, args: arguments, loc: location }.to_json(*opts) + { type: :return, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_return: ((Args | ArgsAddBlock) arguments) -> Return + # on_return: (Args arguments) -> Return def on_return(arguments) - keyword = find_token(Kw, 'return') + keyword = find_token(Kw, "return") Return.new( arguments: arguments, @@ -7187,29 +10122,45 @@ class Return0 # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('return0') + q.group(2, "(", ")") do + q.text("return0") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :return0, value: value, loc: location }.to_json(*opts) + { type: :return0, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_return0: () -> Return0 def on_return0 - keyword = find_token(Kw, 'return') + keyword = find_token(Kw, "return") Return0.new(value: keyword.value, location: keyword.location) end @@ -7226,19 +10177,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('rparen') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@rparen, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -7271,21 +10209,42 @@ class SClass # [Location] the location of this node attr_reader :location - def initialize(target:, bodystmt:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, bodystmt:, location:, comments: []) @target = target @bodystmt = bodystmt @location = location + @comments = comments + end + + def child_nodes + [target, bodystmt] + end + + def format(q) + q.group(0, "class << ", "end") do + q.format(target) + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + q.breakable(force: true) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('sclass') + q.group(2, "(", ")") do + q.text("sclass") q.breakable q.pp(target) q.breakable q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) end end @@ -7294,7 +10253,8 @@ def to_json(*opts) type: :sclass, target: target, bodystmt: bodystmt, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -7302,8 +10262,8 @@ def to_json(*opts) # :call-seq: # on_sclass: (untyped target, BodyStmt bodystmt) -> SClass def on_sclass(target, bodystmt) - beginning = find_token(Kw, 'class') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "class") + ending = find_token(Kw, "end") bodystmt.bind( find_next_statement_start(target.location.end_char), @@ -7330,7 +10290,14 @@ def on_sclass(target, bodystmt) # parent stmts node as well as an stmt which can be any expression in # Ruby. def on_stmts_add(statements, statement) - statements << statement + location = + if statements.body.empty? + statement.location + else + statements.location.to(statement.location) + end + + Statements.new(self, body: statements.body << statement, location: location) end # Everything that has a block of code inside of it has a list of statements. @@ -7341,19 +10308,23 @@ def on_stmts_add(statements, statement) # propagate that onto void_stmt nodes inside the stmts in order to make sure # all comments get printed appropriately. class Statements - # [Ripper::ParseTree] the parser that created this node + # [SyntaxTree] the parser that is generating this node attr_reader :parser - # [Array[untyped]] the list of expressions contained within this node + # [Array[ untyped ]] the list of expressions contained within this node attr_reader :body # [Location] the location of this node attr_reader :location - def initialize(parser:, body:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parser, body:, location:, comments: []) @parser = parser @body = body @location = location + @comments = comments end def bind(start_char, end_char) @@ -7391,42 +10362,106 @@ def bind_end(end_char) ) end - def <<(statement) - @location = - body.any? ? location.to(statement.location) : statement.location + def empty? + body.all? do |statement| + statement.is_a?(VoidStmt) && statement.comments.empty? + end + end + + def child_nodes + body + end + + def format(q) + line = nil + + # This handles a special case where you've got a block of statements where + # the only value is a comment. In that case a lot of nodes like + # brace_block will attempt to format as a single line, but since that + # wouldn't work with a comment, we intentionally break the parent group. + if body.length == 2 && body.first.is_a?(VoidStmt) + q.format(body.last) + q.break_parent + return + end + + body.each_with_index do |statement, index| + next if statement.is_a?(VoidStmt) + + if line.nil? + q.format(statement) + elsif (statement.location.start_line - line) > 1 + q.breakable(force: true) + q.breakable(force: true) + q.format(statement) + elsif statement.is_a?(AccessCtrl) || body[index - 1].is_a?(AccessCtrl) + q.breakable(force: true) + q.breakable(force: true) + q.format(statement) + elsif statement.location.start_line != line + q.breakable(force: true) + q.format(statement) + elsif !q.parent.is_a?(StringEmbExpr) + q.breakable(force: true) + q.format(statement) + else + q.text("; ") + q.format(statement) + end - body << statement - self + line = statement.location.end_line + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('statements') + q.group(2, "(", ")") do + q.text("statements") q.breakable q.seplist(body) { |statement| q.pp(statement) } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :stmts, body: body, loc: location }.to_json(*opts) + { type: :statements, body: body, loc: location, cmts: comments }.to_json( + *opts + ) end private + # As efficiently as possible, gather up all of the comments that have been + # found while this statements list was being parsed and add them into the + # body. def attach_comments(start_char, end_char) - attachable = - parser.comments.select do |comment| - comment.is_a?(Comment) && !comment.inline && - start_char <= comment.location.start_char && - end_char >= comment.location.end_char && - !comment.value.include?('prettier-ignore') - end + parser_comments = parser.comments + + comment_index = 0 + body_index = 0 + + while comment_index < parser_comments.size + comment = parser_comments[comment_index] + location = comment.location - return if attachable.empty? + if !comment.inline? && (start_char <= location.start_char) && + (end_char >= location.end_char) + parser_comments.delete_at(comment_index) - parser.comments -= attachable - @body = (body + attachable).sort_by! { |node| node.location.start_char } + while (node = body[body_index]) && + ( + node.is_a?(VoidStmt) || + node.location.start_char < location.start_char + ) + body_index += 1 + end + + body.insert(body_index, comment) + else + comment_index += 1 + end + end end end @@ -7434,7 +10469,7 @@ def attach_comments(start_char, end_char) # on_stmts_new: () -> Statements def on_stmts_new Statements.new( - parser: self, + self, body: [], location: Location.fixed(line: lineno, char: char_pos) ) @@ -7445,29 +10480,16 @@ def on_stmts_new # "string" # class StringContent - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the # string attr_reader :parts # [Location] the location of this node - attr_reader :location - - def initialize(parts:, location:) - @parts = parts - @location = location - end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('string') - - q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } - end - end + attr_reader :location - def to_json(*opts) - { type: :string, parts: parts, loc: location }.to_json(*opts) + def initialize(parts:, location:) + @parts = parts + @location = location end end @@ -7499,28 +10521,53 @@ class StringConcat # [Location] the location of this node attr_reader :location - def initialize(left:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) @left = left @right = right @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + def format(q) + q.group do + q.format(left) + q.text(' \\') + q.indent do + q.breakable(force: true) + q.format(right) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('string_concat') + q.group(2, "(", ")") do + q.text("string_concat") q.breakable q.pp(left) q.breakable q.pp(right) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :string_concat, left: left, right: right, loc: location }.to_json( - *opts - ) + { + type: :string_concat, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -7559,22 +10606,43 @@ class StringDVar # [Location] the location of this node attr_reader :location - def initialize(variable:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(variable:, location:, comments: []) @variable = variable @location = location + @comments = comments + end + + def child_nodes + [variable] + end + + def format(q) + q.text('#{') + q.format(variable) + q.text("}") end def pretty_print(q) - q.group(2, '(', ')') do - q.text('string_dvar') + q.group(2, "(", ")") do + q.text("string_dvar") q.breakable q.pp(variable) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :string_dvar, var: variable, loc: location }.to_json(*opts) + { + type: :string_dvar, + var: variable, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -7602,22 +10670,58 @@ class StringEmbExpr # [Location] the location of this node attr_reader :location - def initialize(statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statements:, location:, comments: []) @statements = statements @location = location + @comments = comments + end + + def child_nodes + [statements] + end + + def format(q) + if location.start_line == location.end_line + # If the contents of this embedded expression were originally on the + # same line in the source, then we're going to leave them in place and + # assume that's the way the developer wanted this expression + # represented. + doc = q.group(0, '#{', "}") { q.format(statements) } + RemoveBreaks.call(doc) + else + q.group do + q.text('#{') + q.indent do + q.breakable("") + q.format(statements) + end + q.breakable("") + q.text("}") + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('string_embexpr') + q.group(2, "(", ")") do + q.text("string_embexpr") q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :string_embexpr, stmts: statements, loc: location }.to_json(*opts) + { + type: :string_embexpr, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -7643,7 +10747,7 @@ def on_string_embexpr(statements) # "string" # class StringLiteral - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the # string literal attr_reader :parts @@ -7653,18 +10757,58 @@ class StringLiteral # [Location] the location of this node attr_reader :location - def initialize(parts:, quote:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, quote:, location:, comments: []) @parts = parts @quote = quote @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + if parts.empty? + q.text("#{q.quote}#{q.quote}") + return + end + + opening_quote, closing_quote = + if !Quotes.locked?(self) + [q.quote, q.quote] + elsif quote.start_with?("%") + [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])] + else + [quote, quote] + end + + q.group(0, opening_quote, closing_quote) do + parts.each do |part| + if part.is_a?(TStringContent) + value = Quotes.normalize(part.value, closing_quote) + separator = -> { q.breakable(force: true, indent: false) } + q.seplist(value.split(/\r?\n/, -1), separator) do |text| + q.text(text) + end + else + q.format(part) + end + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('string_literal') + q.group(2, "(", ")") do + q.text("string_literal") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end @@ -7673,7 +10817,8 @@ def to_json(*opts) type: :string_literal, parts: parts, quote: quote, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -7710,35 +10855,60 @@ def on_string_literal(string) # super(value) # class Super - # [ArgParen | Args | ArgsAddBlock] the arguments to the keyword + # [ArgParen | Args] the arguments to the keyword attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + def format(q) + q.group do + q.text("super") + + if arguments.is_a?(ArgParen) + q.format(arguments) + else + q.text(" ") + q.nest("super ".length) { q.format(arguments) } + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('super') + q.group(2, "(", ")") do + q.text("super") q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :super, args: arguments, loc: location }.to_json(*opts) + { type: :super, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_super: ((ArgParen | Args | ArgsAddBlock) arguments) -> Super + # on_super: ((ArgParen | Args) arguments) -> Super def on_super(arguments) - keyword = find_token(Kw, 'super') + keyword = find_token(Kw, "super") Super.new( arguments: arguments, @@ -7773,19 +10943,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('symbeg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@symbeg, value: value, loc: location }.to_json(*opts) - end end # symbeg is a token that represents the beginning of a symbol literal. @@ -7819,19 +10976,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('symbol') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :symbol, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -7839,7 +10983,7 @@ def to_json(*opts) # (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value # ) -> SymbolContent def on_symbol(value) - tokens.pop + tokens.delete(value) SymbolContent.new(value: value, location: value.location) end @@ -7857,22 +11001,42 @@ class SymbolLiteral # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.text(":") + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('symbol_literal') + q.group(2, "(", ")") do + q.text("symbol_literal") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :symbol_literal, value: value, loc: location }.to_json(*opts) + { + type: :symbol_literal, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -7884,15 +11048,16 @@ def to_json(*opts) # ) value # ) -> SymbolLiteral def on_symbol_literal(value) - if tokens[-1] == value - SymbolLiteral.new(value: tokens.pop, location: value.location) - else + if value.is_a?(SymbolContent) symbeg = find_token(SymBeg) SymbolLiteral.new( value: value.value, location: symbeg.location.to(value.location) ) + else + tokens.delete(value) + SymbolLiteral.new(value: value, location: value.location) end end @@ -7901,28 +11066,55 @@ def on_symbol_literal(value) # %I[one two three] # class Symbols - # [Array[Word]] the words in the symbol array literal + # [Array[ Word ]] the words in the symbol array literal attr_reader :elements # [Location] the location of this node attr_reader :location - def initialize(elements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(elements:, location:, comments: []) @elements = elements @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.group(0, "%I[", "]") do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('symbols') + q.group(2, "(", ")") do + q.text("symbols") q.breakable - q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } } + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :symbols, elems: elements, loc: location }.to_json(*opts) + { + type: :symbols, + elems: elements, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -7954,19 +11146,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('symbols_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@symbols_beg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -8006,19 +11185,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('tlambda') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@tlambda, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -8051,19 +11217,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('tlambeg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@tlambeg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -8092,24 +11245,37 @@ class TopConstField # [Location] the location of this node attr_reader :location - def initialize(constant:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, location:, comments: []) @constant = constant @location = location + @comments = comments + end + + def child_nodes + [constant] end def pretty_print(q) - q.group(2, '(', ')') do - q.text('top_const_field') + q.group(2, "(", ")") do + q.text("top_const_field") q.breakable q.pp(constant) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :top_const_field, constant: constant, loc: location }.to_json( - *opts - ) + { + type: :top_const_field, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -8136,22 +11302,42 @@ class TopConstRef # [Location] the location of this node attr_reader :location - def initialize(constant:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, location:, comments: []) @constant = constant @location = location + @comments = comments + end + + def child_nodes + [constant] + end + + def format(q) + q.text("::") + q.format(constant) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('top_const_ref') + q.group(2, "(", ")") do + q.text("top_const_ref") q.breakable q.pp(constant) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :top_const_ref, constant: constant, loc: location }.to_json(*opts) + { + type: :top_const_ref, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -8187,19 +11373,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('tstring_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@tstring_beg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -8230,25 +11403,40 @@ class TStringContent # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('tstring_content') + q.group(2, "(", ")") do + q.text("tstring_content") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) { - type: :@tstring_content, - value: value.force_encoding('UTF-8'), - loc: location + type: :tstring_content, + value: value.force_encoding("UTF-8"), + loc: location, + cmts: comments }.to_json(*opts) end end @@ -8283,19 +11471,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('tstring_end') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@tstring_end, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -8325,28 +11500,44 @@ class Not # [Location] the location of this node attr_reader :location - def initialize(statement:, parentheses:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, parentheses:, location:, comments: []) @statement = statement @parentheses = parentheses @location = location + @comments = comments + end + + def child_nodes + [statement] + end + + def format(q) + q.text(parentheses ? "not(" : "not ") + q.format(statement) + q.text(")") if parentheses end def pretty_print(q) - q.group(2, '(', ')') do - q.text('not') + q.group(2, "(", ")") do + q.text("not") q.breakable q.pp(statement) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) { - type: :unary, - op: :not, + type: :not, value: statement, paren: parentheses, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -8366,28 +11557,47 @@ class Unary # [Location] the location of this node attr_reader :location - def initialize(operator:, statement:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(operator:, statement:, location:, comments: []) @operator = operator @statement = statement @location = location + @comments = comments + end + + def child_nodes + [statement] + end + + def format(q) + q.text(operator) + q.format(statement) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('unary') + q.group(2, "(", ")") do + q.text("unary") q.breakable q.pp(operator) q.breakable q.pp(statement) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :unary, op: operator, value: statement, loc: location }.to_json( - *opts - ) + { + type: :unary, + op: operator, + value: statement, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -8399,11 +11609,11 @@ def on_unary(operator, statement) # We have somewhat special handling of the not operator since if it has # parentheses they don't get reported as a paren node for some reason. - beginning = find_token(Kw, 'not') + beginning = find_token(Kw, "not") ending = statement range = beginning.location.end_char...statement.location.start_char - paren = source[range].include?('(') + paren = source[range].include?("(") if paren find_token(LParen) @@ -8442,35 +11652,80 @@ def on_unary(operator, statement) # undef method # class Undef - # [Array[DynaSymbol | SymbolLiteral]] the symbols to undefine + class UndefArgumentFormatter + # [DynaSymbol | SymbolLiteral] the symbol to undefine + attr_reader :node + + def initialize(node) + @node = node + end + + def comments + if node.is_a?(SymbolLiteral) + node.comments + node.value.comments + else + node.comments + end + end + + def format(q) + node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node) + end + end + + # [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine attr_reader :symbols # [Location] the location of this node attr_reader :location - def initialize(symbols:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(symbols:, location:, comments: []) @symbols = symbols @location = location + @comments = comments + end + + def child_nodes + symbols + end + + def format(q) + keyword = "undef " + formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) } + + q.group do + q.text(keyword) + q.nest(keyword.length) do + q.seplist(formatters) { |formatter| q.format(formatter) } + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('undef') + q.group(2, "(", ")") do + q.text("undef") q.breakable - q.group(2, '(', ')') { q.seplist(symbols) { |symbol| q.pp(symbol) } } + q.group(2, "(", ")") { q.seplist(symbols) { |symbol| q.pp(symbol) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :undef, syms: symbols, loc: location }.to_json(*opts) + { type: :undef, syms: symbols, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_undef: (Array[DynaSymbol | SymbolLiteral] symbols) -> Undef def on_undef(symbols) - keyword = find_token(Kw, 'undef') + keyword = find_token(Kw, "undef") Undef.new( symbols: symbols, @@ -8496,16 +11751,34 @@ class Unless # [Location] the location of this node attr_reader :location - def initialize(predicate:, statements:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + predicate:, + statements:, + consequent:, + location:, + comments: [] + ) @predicate = predicate @statements = statements @consequent = consequent @location = location + @comments = comments + end + + def child_nodes + [predicate, statements, consequent] + end + + def format(q) + ConditionalFormatter.new("unless", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('unless') + q.group(2, "(", ")") do + q.text("unless") q.breakable q.pp(predicate) @@ -8517,6 +11790,8 @@ def pretty_print(q) q.breakable q.pp(consequent) end + + q.pp(Comment::List.new(comments)) end end @@ -8526,7 +11801,8 @@ def to_json(*opts) pred: predicate, stmts: statements, cons: consequent, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -8538,8 +11814,8 @@ def to_json(*opts) # ((nil | Elsif | Else) consequent) # ) -> Unless def on_unless(predicate, statements, consequent) - beginning = find_token(Kw, 'unless') - ending = consequent || find_token(Kw, 'end') + beginning = find_token(Kw, "unless") + ending = consequent || find_token(Kw, "end") statements.bind(predicate.location.end_char, ending.location.start_char) @@ -8565,21 +11841,35 @@ class UnlessMod # [Location] the location of this node attr_reader :location - def initialize(statement:, predicate:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) @statement = statement @predicate = predicate @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + def format(q) + ConditionalModFormatter.new("unless", self).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('unless_mod') + q.group(2, "(", ")") do + q.text("unless_mod") q.breakable q.pp(statement) q.breakable q.pp(predicate) + + q.pp(Comment::List.new(comments)) end end @@ -8588,7 +11878,8 @@ def to_json(*opts) type: :unless_mod, stmt: statement, pred: predicate, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -8596,7 +11887,7 @@ def to_json(*opts) # :call-seq: # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod def on_unless_mod(predicate, statement) - find_token(Kw, 'unless') + find_token(Kw, "unless") UnlessMod.new( statement: statement, @@ -8605,6 +11896,43 @@ def on_unless_mod(predicate, statement) ) end + # Formats an Until, UntilMod, While, or WhileMod node. + class LoopFormatter + # [String] the name of the keyword used for this loop + attr_reader :keyword + + # [Until | UntilMod | While | WhileMod] the node that is being formatted + attr_reader :node + + # [untyped] the statements associated with the node + attr_reader :statements + + def initialize(keyword, node, statements) + @keyword = keyword + @node = node + @statements = statements + end + + def format(q) + q.group do + q.if_break do + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } + q.indent do + q.breakable("") + q.format(statements) + end + q.breakable("") + q.text("end") + end.if_flat do + q.format(statements) + q.text(" #{keyword} ") + q.format(node.predicate) + end + end + end + end + # Until represents an +until+ loop. # # until predicate @@ -8620,21 +11948,46 @@ class Until # [Location] the location of this node attr_reader :location - def initialize(predicate:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(predicate:, statements:, location:, comments: []) @predicate = predicate @statements = statements @location = location + @comments = comments + end + + def child_nodes + [predicate, statements] + end + + def format(q) + if statements.empty? + keyword = "until " + + q.group do + q.text(keyword) + q.nest(keyword.length) { q.format(predicate) } + q.breakable(force: true) + q.text("end") + end + else + LoopFormatter.new("until", self, statements).format(q) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('until') + q.group(2, "(", ")") do + q.text("until") q.breakable q.pp(predicate) q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -8643,7 +11996,8 @@ def to_json(*opts) type: :until, pred: predicate, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -8651,12 +12005,12 @@ def to_json(*opts) # :call-seq: # on_until: (untyped predicate, Statements statements) -> Until def on_until(predicate, statements) - beginning = find_token(Kw, 'until') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "until") + ending = find_token(Kw, "end") # Consume the do keyword if it exists so that it doesn't get confused for # some other block - keyword = find_token(Kw, 'do', consume: false) + keyword = find_token(Kw, "do", consume: false) if keyword && keyword.location.start_char > predicate.location.end_char && keyword.location.end_char < ending.location.start_char tokens.delete(keyword) @@ -8686,21 +12040,35 @@ class UntilMod # [Location] the location of this node attr_reader :location - def initialize(statement:, predicate:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) @statement = statement @predicate = predicate @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + def format(q) + LoopFormatter.new("until", self, statement).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('until_mod') + q.group(2, "(", ")") do + q.text("until_mod") q.breakable q.pp(statement) q.breakable q.pp(predicate) + + q.pp(Comment::List.new(comments)) end end @@ -8709,7 +12077,8 @@ def to_json(*opts) type: :until_mod, stmt: statement, pred: predicate, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -8717,7 +12086,7 @@ def to_json(*opts) # :call-seq: # on_until_mod: (untyped predicate, untyped statement) -> UntilMod def on_until_mod(predicate, statement) - find_token(Kw, 'until') + find_token(Kw, "until") UntilMod.new( statement: statement, @@ -8741,35 +12110,58 @@ class VarAlias # [Location] the location of this node attr_reader :location - def initialize(left:, right:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) @left = left @right = right @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + def format(q) + keyword = "alias " + + q.text(keyword) + q.format(left) + q.text(" ") + q.format(right) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('var_alias') + q.group(2, "(", ")") do + q.text("var_alias") q.breakable q.pp(left) q.breakable q.pp(right) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :var_alias, left: left, right: right, loc: location }.to_json( - *opts - ) + { + type: :var_alias, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) end end # :call-seq: # on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias def on_var_alias(left, right) - keyword = find_token(Kw, 'alias') + keyword = find_token(Kw, "alias") VarAlias.new( left: left, @@ -8791,22 +12183,38 @@ class VarField # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.format(value) if value end def pretty_print(q) - q.group(2, '(', ')') do - q.text('var_field') + q.group(2, "(", ")") do + q.text("var_field") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :var_field, value: value, loc: location }.to_json(*opts) + { type: :var_field, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -8829,7 +12237,7 @@ def on_var_field(value) # VarRef represents a variable reference. # - # variable + # true # # This can be a plain local variable like the example above. It can also be a # constant, a class variable, a global variable, an instance variable, a @@ -8842,22 +12250,38 @@ class VarRef # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('var_ref') + q.group(2, "(", ")") do + q.text("var_ref") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :var_ref, value: value, loc: location }.to_json(*opts) + { type: :var_ref, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -8879,22 +12303,41 @@ class AccessCtrl # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('access_ctrl') + q.group(2, "(", ")") do + q.text("access_ctrl") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :access_ctrl, value: value, loc: location }.to_json(*opts) + { + type: :access_ctrl, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -8910,22 +12353,38 @@ class VCall # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [value] + end + + def format(q) + q.format(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('vcall') + q.group(2, "(", ")") do + q.text("vcall") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :vcall, value: value, loc: location }.to_json(*opts) + { type: :vcall, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -8953,16 +12412,26 @@ class VoidStmt # [Location] the location of this node attr_reader :location - def initialize(location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(location:, comments: []) @location = location + @comments = comments + end + + def format(q) end def pretty_print(q) - q.group(2, '(', ')') { q.text('void_stmt') } + q.group(2, "(", ")") do + q.text("void_stmt") + q.pp(Comment::List.new(comments)) + end end def to_json(*opts) - { type: :void_stmt, loc: location }.to_json(*opts) + { type: :void_stmt, loc: location, cmts: comments }.to_json(*opts) end end @@ -8979,7 +12448,7 @@ def on_void_stmt # end # class When - # [untyped] the arguments to the when clause + # [Args] the arguments to the when clause attr_reader :arguments # [Statements] the expressions to be executed @@ -8991,16 +12460,60 @@ class When # [Location] the location of this node attr_reader :location - def initialize(arguments:, statements:, consequent:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + arguments:, + statements:, + consequent:, + location:, + comments: [] + ) @arguments = arguments @statements = statements @consequent = consequent @location = location + @comments = comments + end + + def child_nodes + [arguments, statements, consequent] + end + + def format(q) + keyword = "when " + + q.group do + q.group do + q.text(keyword) + q.nest(keyword.length) do + if arguments.comments.any? + q.format(arguments) + else + separator = -> { q.group { q.comma_breakable } } + q.seplist(arguments.parts, separator) { |part| q.format(part) } + end + end + end + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.breakable(force: true) + q.format(consequent) + end + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('when') + q.group(2, "(", ")") do + q.text("when") q.breakable q.pp(arguments) @@ -9012,6 +12525,8 @@ def pretty_print(q) q.breakable q.pp(consequent) end + + q.pp(Comment::List.new(comments)) end end @@ -9021,20 +12536,21 @@ def to_json(*opts) args: arguments, stmts: statements, cons: consequent, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end # :call-seq: # on_when: ( - # untyped arguments, + # Args arguments, # Statements statements, # (nil | Else | When) consequent # ) -> When def on_when(arguments, statements, consequent) - beginning = find_token(Kw, 'when') - ending = consequent || find_token(Kw, 'end') + beginning = find_token(Kw, "when") + ending = consequent || find_token(Kw, "end") statements.bind(arguments.location.end_char, ending.location.start_char) @@ -9061,21 +12577,46 @@ class While # [Location] the location of this node attr_reader :location - def initialize(predicate:, statements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(predicate:, statements:, location:, comments: []) @predicate = predicate @statements = statements @location = location + @comments = comments + end + + def child_nodes + [predicate, statements] + end + + def format(q) + if statements.empty? + keyword = "while " + + q.group do + q.text(keyword) + q.nest(keyword.length) { q.format(predicate) } + q.breakable(force: true) + q.text("end") + end + else + LoopFormatter.new("while", self, statements).format(q) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('while') + q.group(2, "(", ")") do + q.text("while") q.breakable q.pp(predicate) q.breakable q.pp(statements) + + q.pp(Comment::List.new(comments)) end end @@ -9084,7 +12625,8 @@ def to_json(*opts) type: :while, pred: predicate, stmts: statements, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -9092,12 +12634,12 @@ def to_json(*opts) # :call-seq: # on_while: (untyped predicate, Statements statements) -> While def on_while(predicate, statements) - beginning = find_token(Kw, 'while') - ending = find_token(Kw, 'end') + beginning = find_token(Kw, "while") + ending = find_token(Kw, "end") # Consume the do keyword if it exists so that it doesn't get confused for # some other block - keyword = find_token(Kw, 'do', consume: false) + keyword = find_token(Kw, "do", consume: false) if keyword && keyword.location.start_char > predicate.location.end_char && keyword.location.end_char < ending.location.start_char tokens.delete(keyword) @@ -9127,21 +12669,35 @@ class WhileMod # [Location] the location of this node attr_reader :location - def initialize(statement:, predicate:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) @statement = statement @predicate = predicate @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + def format(q) + LoopFormatter.new("while", self, statement).format(q) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('while_mod') + q.group(2, "(", ")") do + q.text("while_mod") q.breakable q.pp(statement) q.breakable q.pp(predicate) + + q.pp(Comment::List.new(comments)) end end @@ -9150,7 +12706,8 @@ def to_json(*opts) type: :while_mod, stmt: statement, pred: predicate, - loc: location + loc: location, + cmts: comments }.to_json(*opts) end end @@ -9158,7 +12715,7 @@ def to_json(*opts) # :call-seq: # on_while_mod: (untyped predicate, untyped statement) -> WhileMod def on_while_mod(predicate, statement) - find_token(Kw, 'while') + find_token(Kw, "while") WhileMod.new( statement: statement, @@ -9170,33 +12727,50 @@ def on_while_mod(predicate, statement) # Word represents an element within a special array literal that accepts # interpolation. # - # %w[a#{b}c xyz] + # %W[a#{b}c xyz] # # In the example above, there would be two Word nodes within a parent Words # node. class Word - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the word + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # word attr_reader :parts # [Location] the location of this node attr_reader :location - def initialize(parts:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) @parts = parts @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + q.format_each(parts) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('word') + q.group(2, "(", ")") do + q.text("word") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :word, parts: parts, loc: location }.to_json(*opts) + { type: :word, parts: parts, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -9223,28 +12797,52 @@ def on_word_new # %W[one two three] # class Words - # [Array[Word]] the elements of this array + # [Array[ Word ]] the elements of this array attr_reader :elements # [Location] the location of this node attr_reader :location - def initialize(elements:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(elements:, location:, comments: []) @elements = elements @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.group(0, "%W[", "]") do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('words') + q.group(2, "(", ")") do + q.text("words") q.breakable - q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } } + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :words, elems: elements, loc: location }.to_json(*opts) + { type: :words, elems: elements, loc: location, cmts: comments }.to_json( + *opts + ) end end @@ -9276,19 +12874,6 @@ def initialize(value:, location:) @value = value @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('words_beg') - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :@words_beg, value: value, loc: location }.to_json(*opts) - end end # :call-seq: @@ -9321,7 +12906,7 @@ def on_words_new # `ls` # class XString - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the # xstring attr_reader :parts @@ -9332,19 +12917,6 @@ def initialize(parts:, location:) @parts = parts @location = location end - - def pretty_print(q) - q.group(2, '(', ')') do - q.text('xstring') - - q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } - end - end - - def to_json(*opts) - { type: :xstring, parts: parts, loc: location }.to_json(*opts) - end end # :call-seq: @@ -9365,7 +12937,7 @@ def on_xstring_new heredoc = @heredocs[-1] location = - if heredoc && heredoc.beginning.value.include?('`') + if heredoc && heredoc.beginning.value.include?("`") heredoc.location else find_token(Backtick).location @@ -9379,29 +12951,50 @@ def on_xstring_new # `ls` # class XStringLiteral - # [Array[StringEmbExpr | StringDVar | TStringContent]] the parts of the + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the # xstring attr_reader :parts # [Location] the location of this node attr_reader :location - def initialize(parts:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) @parts = parts @location = location + @comments = comments + end + + def child_nodes + parts + end + + def format(q) + q.text("`") + q.format_each(parts) + q.text("`") end def pretty_print(q) - q.group(2, '(', ')') do - q.text('xstring_literal') + q.group(2, "(", ")") do + q.text("xstring_literal") q.breakable - q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } } + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :xstring_literal, parts: parts, loc: location }.to_json(*opts) + { + type: :xstring_literal, + parts: parts, + loc: location, + cmts: comments + }.to_json(*opts) end end @@ -9410,7 +13003,7 @@ def to_json(*opts) def on_xstring_literal(xstring) heredoc = @heredocs[-1] - if heredoc && heredoc.beginning.value.include?('`') + if heredoc && heredoc.beginning.value.include?("`") Heredoc.new( beginning: heredoc.beginning, ending: heredoc.ending, @@ -9432,35 +13025,55 @@ def on_xstring_literal(xstring) # yield value # class Yield - # [ArgsAddBlock | Paren] the arguments passed to the yield + # [Args | Paren] the arguments passed to the yield attr_reader :arguments # [Location] the location of this node attr_reader :location - def initialize(arguments:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) @arguments = arguments @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + def format(q) + q.group do + q.text("yield") + q.text(" ") if arguments.is_a?(Args) + q.format(arguments) + end end def pretty_print(q) - q.group(2, '(', ')') do - q.text('yield') + q.group(2, "(", ")") do + q.text("yield") q.breakable q.pp(arguments) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :yield, args: arguments, loc: location }.to_json(*opts) + { type: :yield, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: - # on_yield: ((ArgsAddBlock | Paren) arguments) -> Yield + # on_yield: ((Args | Paren) arguments) -> Yield def on_yield(arguments) - keyword = find_token(Kw, 'yield') + keyword = find_token(Kw, "yield") Yield.new( arguments: arguments, @@ -9479,29 +13092,45 @@ class Yield0 # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('yield0') + q.group(2, "(", ")") do + q.text("yield0") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :yield0, value: value, loc: location }.to_json(*opts) + { type: :yield0, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_yield0: () -> Yield0 def on_yield0 - keyword = find_token(Kw, 'yield') + keyword = find_token(Kw, "yield") Yield0.new(value: keyword.value, location: keyword.location) end @@ -9514,32 +13143,48 @@ class ZSuper # [String] the value of the keyword attr_reader :value - # [Location] the location of the node + # [Location] the location of this node attr_reader :location - def initialize(value:, location:) + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) @value = value @location = location + @comments = comments + end + + def child_nodes + [] + end + + def format(q) + q.text(value) end def pretty_print(q) - q.group(2, '(', ')') do - q.text('zsuper') + q.group(2, "(", ")") do + q.text("zsuper") q.breakable q.pp(value) + + q.pp(Comment::List.new(comments)) end end def to_json(*opts) - { type: :zsuper, value: value, loc: location }.to_json(*opts) + { type: :zsuper, value: value, loc: location, cmts: comments }.to_json( + *opts + ) end end # :call-seq: # on_zsuper: () -> ZSuper def on_zsuper - keyword = find_token(Kw, 'super') + keyword = find_token(Kw, "super") ZSuper.new(value: keyword.value, location: keyword.location) end diff --git a/lib/syntax_tree/prettyprint.rb b/lib/syntax_tree/prettyprint.rb new file mode 100644 index 00000000..2805b724 --- /dev/null +++ b/lib/syntax_tree/prettyprint.rb @@ -0,0 +1,1132 @@ +# frozen_string_literal: true +# +# This class implements a pretty printing algorithm. It finds line breaks and +# nice indentations for grouped structure. +# +# By default, the class assumes that primitive elements are strings and each +# byte in the strings is a single column in width. But it can be used for other +# situations by giving suitable arguments for some methods: +# +# * newline object and space generation block for PrettyPrint.new +# * optional width argument for PrettyPrint#text +# * PrettyPrint#breakable +# +# There are several candidate uses: +# * text formatting using proportional fonts +# * multibyte characters which has columns different to number of bytes +# * non-string formatting +# +# == Usage +# +# To use this module, you will need to generate a tree of print nodes that +# represent indentation and newline behavior before it gets sent to the printer. +# Each node has different semantics, depending on the desired output. +# +# The most basic node is a Text node. This represents plain text content that +# cannot be broken up even if it doesn't fit on one line. You would create one +# of those with the text method, as in: +# +# PrettyPrint.format { |q| q.text('my content') } +# +# No matter what the desired output width is, the output for the snippet above +# will always be the same. +# +# If you want to allow the printer to break up the content on the space +# character when there isn't enough width for the full string on the same line, +# you can use the Breakable and Group nodes. For example: +# +# PrettyPrint.format do |q| +# q.group do +# q.text('my') +# q.breakable +# q.text('content') +# end +# end +# +# Now, if everything fits on one line (depending on the maximum width specified) +# then it will be the same output as the first example. If, however, there is +# not enough room on the line, then you will get two lines of output, one for +# the first string and one for the second. +# +# There are other nodes for the print tree as well, described in the +# documentation below. They control alignment, indentation, conditional +# formatting, and more. +# +# == Bugs +# * Box based formatting? +# +# Report any bugs at http://bugs.ruby-lang.org +# +# == References +# Christian Lindig, Strictly Pretty, March 2000, +# https://lindig.github.io/papers/strictly-pretty-2000.pdf +# +# Philip Wadler, A prettier printer, March 1998, +# https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf +# +# == Author +# Tanaka Akira +# +class PrettyPrint + # A node in the print tree that represents aligning nested nodes to a certain + # prefix width or string. + class Align + attr_reader :indent, :contents + + def initialize(indent:, contents: []) + @indent = indent + @contents = contents + end + + def pretty_print(q) + q.group(2, "align([", "])") do + q.seplist(contents) { |content| q.pp(content) } + end + end + end + + # A node in the print tree that represents a place in the buffer that the + # content can be broken onto multiple lines. + class Breakable + attr_reader :separator, :width + + def initialize( + separator = " ", + width = separator.length, + force: false, + indent: true + ) + @separator = separator + @width = width + @force = force + @indent = indent + end + + def force? + @force + end + + def indent? + @indent + end + + def pretty_print(q) + q.text("breakable") + + attributes = + [("force=true" if force?), ("indent=false" unless indent?)].compact + + if attributes.any? + q.text("(") + q.seplist(attributes, -> { q.text(", ") }) do |attribute| + q.text(attribute) + end + q.text(")") + end + end + end + + # A node in the print tree that forces the surrounding group to print out in + # the "break" mode as opposed to the "flat" mode. Useful for when you need to + # force a newline into a group. + class BreakParent + def pretty_print(q) + q.text("break-parent") + end + end + + # A node in the print tree that represents a group of items which the printer + # should try to fit onto one line. This is the basic command to tell the + # printer when to break. Groups are usually nested, and the printer will try + # to fit everything on one line, but if it doesn't fit it will break the + # outermost group first and try again. It will continue breaking groups until + # everything fits (or there are no more groups to break). + class Group + attr_reader :depth, :contents + + def initialize(depth, contents: []) + @depth = depth + @contents = contents + @break = false + end + + def break + @break = true + end + + def break? + @break + end + + def pretty_print(q) + q.group(2, "group([", "])") do + q.seplist(contents) { |content| q.pp(content) } + end + end + end + + # A node in the print tree that represents printing one thing if the + # surrounding group node is broken and another thing if the surrounding group + # node is flat. + class IfBreak + attr_reader :break_contents, :flat_contents + + def initialize(break_contents: [], flat_contents: []) + @break_contents = break_contents + @flat_contents = flat_contents + end + + def pretty_print(q) + q.group(2, "if-break(", ")") do + q.breakable("") + q.group(2, "[", "],") do + q.seplist(break_contents) { |content| q.pp(content) } + end + q.breakable + q.group(2, "[", "]") do + q.seplist(flat_contents) { |content| q.pp(content) } + end + end + end + end + + # A node in the print tree that is a variant of the Align node that indents + # its contents by one level. + class Indent + attr_reader :contents + + def initialize(contents: []) + @contents = contents + end + + def pretty_print(q) + q.group(2, "indent([", "])") do + q.seplist(contents) { |content| q.pp(content) } + end + end + end + + # A node in the print tree that has its own special buffer for implementing + # content that should flush before any newline. + # + # Useful for implementating trailing content, as it's not always practical to + # constantly check where the line ends to avoid accidentally printing some + # content after a line suffix node. + class LineSuffix + attr_reader :contents + + def initialize(contents: []) + @contents = contents + end + + def pretty_print(q) + q.group(2, "line-suffix([", "])") do + q.seplist(contents) { |content| q.pp(content) } + end + end + end + + # A node in the print tree that represents plain content that cannot be broken + # up (by default this assumes strings, but it can really be anything). + class Text + attr_reader :objects, :width + + def initialize + @objects = [] + @width = 0 + end + + def add(object: "", width: object.length) + @objects << object + @width += width + end + + def pretty_print(q) + q.group(2, "text([", "])") do + q.seplist(objects) { |object| q.pp(object) } + end + end + end + + # A node in the print tree that represents trimming all of the indentation of + # the current line, in the rare case that you need to ignore the indentation + # that you've already created. This node should be placed after a Breakable. + class Trim + def pretty_print(q) + q.text("trim") + end + end + + # When building up the contents in the output buffer, it's convenient to be + # able to trim trailing whitespace before newlines. If the output object is a + # string or array or strings, then we can do this with some gsub calls. If + # not, then this effectively just wraps the output object and forwards on + # calls to <<. + module Buffer + # This is the default output buffer that provides a base implementation of + # trim! that does nothing. It's effectively a wrapper around whatever output + # object was given to the format command. + class DefaultBuffer + attr_reader :output + + def initialize(output = []) + @output = output + end + + def <<(object) + @output << object + end + + def trim! + 0 + end + end + + # This is an output buffer that wraps a string output object. It provides a + # trim! method that trims off trailing whitespace from the string using + # gsub!. + class StringBuffer < DefaultBuffer + def initialize(output = "".dup) + super(output) + end + + def trim! + length = output.length + output.gsub!(/[\t ]*\z/, "") + length - output.length + end + end + + # This is an output buffer that wraps an array output object. It provides a + # trim! method that trims off trailing whitespace from the last element in + # the array if it's an unfrozen string using the same method as the + # StringBuffer. + class ArrayBuffer < DefaultBuffer + def initialize(output = []) + super(output) + end + + def trim! + return 0 if output.empty? + + trimmed = 0 + + while output.any? && output.last.is_a?(String) && + output.last.match?(/\A[\t ]*\z/) + trimmed += output.pop.length + end + + if output.any? && output.last.is_a?(String) && !output.last.frozen? + length = output.last.length + output.last.gsub!(/[\t ]*\z/, "") + trimmed += length - output.last.length + end + + trimmed + end + end + + # This is a switch for building the correct output buffer wrapper class for + # the given output object. + def self.for(output) + case output + when String + StringBuffer.new(output) + when Array + ArrayBuffer.new(output) + else + DefaultBuffer.new(output) + end + end + end + + # PrettyPrint::SingleLine is used by PrettyPrint.singleline_format + # + # It is passed to be similar to a PrettyPrint object itself, by responding to + # all of the same print tree node builder methods, as well as the #flush + # method. + # + # The significant difference here is that there are no line breaks in the + # output. If an IfBreak node is used, only the flat contents are printed. + # LineSuffix nodes are printed at the end of the buffer when #flush is called. + class SingleLine + # The output object. It stores rendered text and should respond to <<. + attr_reader :output + + # The current array of contents that the print tree builder methods should + # append to. + attr_reader :target + + # A buffer output that wraps any calls to line_suffix that will be flushed + # at the end of printing. + attr_reader :line_suffixes + + # Create a PrettyPrint::SingleLine object + # + # Arguments: + # * +output+ - String (or similar) to store rendered text. Needs to respond + # to '<<'. + # * +maxwidth+ - Argument position expected to be here for compatibility. + # This argument is a noop. + # * +newline+ - Argument position expected to be here for compatibility. + # This argument is a noop. + def initialize(output, maxwidth = nil, newline = nil) + @output = Buffer.for(output) + @target = @output + @line_suffixes = Buffer::ArrayBuffer.new + end + + # Flushes the line suffixes onto the output buffer. + def flush + line_suffixes.output.each { |doc| output << doc } + end + + # -------------------------------------------------------------------------- + # Markers node builders + # -------------------------------------------------------------------------- + + # Appends +separator+ to the text to be output. By default +separator+ is + # ' ' + # + # The +width+, +indent+, and +force+ arguments are here for compatibility. + # They are all noop arguments. + def breakable( + separator = " ", + width = separator.length, + indent: nil, + force: nil + ) + target << separator + end + + # Here for compatibility, does nothing. + def break_parent + end + + # Appends +separator+ to the output buffer. +width+ is a noop here for + # compatibility. + def fill_breakable(separator = " ", width = separator.length) + target << separator + end + + # Immediately trims the output buffer. + def trim + target.trim! + end + + # ---------------------------------------------------------------------------- + # Container node builders + # ---------------------------------------------------------------------------- + + # Opens a block for grouping objects to be pretty printed. + # + # Arguments: + # * +indent+ - noop argument. Present for compatibility. + # * +open_obj+ - text appended before the &block. Default is '' + # * +close_obj+ - text appended after the &block. Default is '' + # * +open_width+ - noop argument. Present for compatibility. + # * +close_width+ - noop argument. Present for compatibility. + def group( + indent = nil, + open_object = "", + close_object = "", + open_width = nil, + close_width = nil + ) + target << open_object + yield + target << close_object + end + + # A class that wraps the ability to call #if_flat. The contents of the + # #if_flat block are executed immediately, so effectively this class and the + # #if_break method that triggers it are unnecessary, but they're here to + # maintain compatibility. + class IfBreakBuilder + def if_flat + yield + end + end + + # Effectively unnecessary, but here for compatibility. + def if_break + IfBreakBuilder.new + end + + # A noop that immediately yields. + def indent + yield + end + + # Changes the target output buffer to the line suffix output buffer which + # will get flushed at the end of printing. + def line_suffix + previous_target, @target = @target, line_suffixes + yield + @target = previous_target + end + + # Takes +indent+ arg, but does nothing with it. + # + # Yields to a block. + def nest(indent) + yield + end + + # Add +object+ to the text to be output. + # + # +width+ argument is here for compatibility. It is a noop argument. + def text(object = "", width = nil) + target << object + end + end + + # This object represents the current level of indentation within the printer. + # It has the ability to generate new levels of indentation through the #align + # and #indent methods. + class IndentLevel + IndentPart = Object.new + DedentPart = Object.new + + StringAlignPart = Struct.new(:n) + NumberAlignPart = Struct.new(:n) + + attr_reader :genspace, :value, :length, :queue, :root + + def initialize( + genspace:, + value: genspace.call(0), + length: 0, + queue: [], + root: nil + ) + @genspace = genspace + @value = value + @length = length + @queue = queue + @root = root + end + + # This can accept a whole lot of different kinds of objects, due to the + # nature of the flexibility of the Align node. + def align(n) + case n + when NilClass + self + when String + indent(StringAlignPart.new(n)) + else + indent(n < 0 ? DedentPart : NumberAlignPart.new(n)) + end + end + + def indent(part = IndentPart) + next_value = genspace.call(0) + next_length = 0 + next_queue = (part == DedentPart ? queue[0...-1] : [*queue, part]) + + last_spaces = 0 + + add_spaces = ->(count) do + next_value << genspace.call(count) + next_length += count + end + + flush_spaces = -> do + add_spaces[last_spaces] if last_spaces > 0 + last_spaces = 0 + end + + next_queue.each do |part| + case part + when IndentPart + flush_spaces.call + add_spaces.call(2) + when StringAlignPart + flush_spaces.call + next_value += part.n + next_length += part.n.length + when NumberAlignPart + last_spaces += part.n + end + end + + flush_spaces.call + + IndentLevel.new( + genspace: genspace, + value: next_value, + length: next_length, + queue: next_queue, + root: root + ) + end + end + + # When printing, you can optionally specify the value that should be used + # whenever a group needs to be broken onto multiple lines. In this case the + # default is \n. + DEFAULT_NEWLINE = "\n" + + # When generating spaces after a newline for indentation, by default we + # generate one space per character needed for indentation. You can change this + # behavior (for instance to use tabs) by passing a different genspace + # procedure. + DEFAULT_GENSPACE = ->(n) { " " * n } + + # There are two modes in printing, break and flat. When we're in break mode, + # any lines will use their newline, any if-breaks will use their break + # contents, etc. + MODE_BREAK = 1 + + # This is another print mode much like MODE_BREAK. When we're in flat mode, we + # attempt to print everything on one line until we either hit a broken group, + # a forced line, or the maximum width. + MODE_FLAT = 2 + + # This is a convenience method which is same as follows: + # + # begin + # q = PrettyPrint.new(output, maxwidth, newline, &genspace) + # ... + # q.flush + # output + # end + # + def self.format( + output = "".dup, + maxwidth = 80, + newline = DEFAULT_NEWLINE, + genspace = DEFAULT_GENSPACE + ) + q = new(output, maxwidth, newline, &genspace) + yield q + q.flush + output + end + + # This is similar to PrettyPrint::format but the result has no breaks. + # + # +maxwidth+, +newline+ and +genspace+ are ignored. + # + # The invocation of +breakable+ in the block doesn't break a line and is + # treated as just an invocation of +text+. + # + def self.singleline_format( + output = "".dup, + maxwidth = nil, + newline = nil, + genspace = nil + ) + q = SingleLine.new(output) + yield q + output + end + + # The output object. It represents the final destination of the contents of + # the print tree. It should respond to <<. + # + # This defaults to "".dup + attr_reader :output + + # This is an output buffer that wraps the output object and provides + # additional functionality depending on its type. + # + # This defaults to Buffer::StringBuffer.new("".dup) + attr_reader :buffer + + # The maximum width of a line, before it is separated in to a newline + # + # This defaults to 80, and should be an Integer + attr_reader :maxwidth + + # The value that is appended to +output+ to add a new line. + # + # This defaults to "\n", and should be String + attr_reader :newline + + # An object that responds to call that takes one argument, of an Integer, and + # returns the corresponding number of spaces. + # + # By default this is: ->(n) { ' ' * n } + attr_reader :genspace + + # The stack of groups that are being printed. + attr_reader :groups + + # The current array of contents that calls to methods that generate print tree + # nodes will append to. + attr_reader :target + + # Creates a buffer for pretty printing. + # + # +output+ is an output target. If it is not specified, '' is assumed. It + # should have a << method which accepts the first argument +obj+ of + # PrettyPrint#text, the first argument +separator+ of PrettyPrint#breakable, + # the first argument +newline+ of PrettyPrint.new, and the result of a given + # block for PrettyPrint.new. + # + # +maxwidth+ specifies maximum line length. If it is not specified, 80 is + # assumed. However actual outputs may overflow +maxwidth+ if long + # non-breakable texts are provided. + # + # +newline+ is used for line breaks. "\n" is used if it is not specified. + # + # The block is used to generate spaces. ->(n) { ' ' * n } is used if it is not + # given. + def initialize( + output = "".dup, + maxwidth = 80, + newline = DEFAULT_NEWLINE, + &genspace + ) + @output = output + @buffer = Buffer.for(output) + @maxwidth = maxwidth + @newline = newline + @genspace = genspace || DEFAULT_GENSPACE + reset + end + + # Returns the group most recently added to the stack. + # + # Contrived example: + # out = "" + # => "" + # q = PrettyPrint.new(out) + # => # + # q.group { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # } + # } + # } + # } + # => 284 + # puts out + # # + # # + # # + # # + def current_group + groups.last + end + + # Flushes all of the generated print tree onto the output buffer, then clears + # the generated tree from memory. + def flush + # First, get the root group, since we placed one at the top to begin with. + doc = groups.first + + # This represents how far along the current line we are. It gets reset + # back to 0 when we encounter a newline. + position = 0 + + # This is our command stack. A command consists of a triplet of an + # indentation level, the mode (break or flat), and a doc node. + commands = [[IndentLevel.new(genspace: genspace), MODE_BREAK, doc]] + + # This is a small optimization boolean. It keeps track of whether or not + # when we hit a group node we should check if it fits on the same line. + should_remeasure = false + + # This is a separate command stack that includes the same kind of triplets + # as the commands variable. It is used to keep track of things that should + # go at the end of printed lines once the other doc nodes are + # accounted for. Typically this is used to implement comments. + line_suffixes = [] + + # This is a linear stack instead of a mutually recursive call defined on + # the individual doc nodes for efficiency. + while commands.any? + indent, mode, doc = commands.pop + + case doc + when Text + doc.objects.each { |object| buffer << object } + position += doc.width + when Array + doc.reverse_each { |part| commands << [indent, mode, part] } + when Indent + commands << [indent.indent, mode, doc.contents] + when Align + commands << [indent.align(doc.indent), mode, doc.contents] + when Trim + position -= buffer.trim! + when Group + if mode == MODE_FLAT && !should_remeasure + commands << + [indent, doc.break? ? MODE_BREAK : MODE_FLAT, doc.contents] + else + should_remeasure = false + next_cmd = [indent, MODE_FLAT, doc.contents] + + if !doc.break? && fits?(next_cmd, commands, maxwidth - position) + commands << next_cmd + else + commands << [indent, MODE_BREAK, doc.contents] + end + end + when IfBreak + if mode == MODE_BREAK + commands << [indent, mode, doc.break_contents] if doc.break_contents + elsif mode == MODE_FLAT + commands << [indent, mode, doc.flat_contents] if doc.flat_contents + end + when LineSuffix + line_suffixes << [indent, mode, doc.contents] + when Breakable + if mode == MODE_FLAT + if doc.force? + # This line was forced into the output even if we were in flat mode, + # so we need to tell the next group that no matter what, it needs to + # remeasure because the previous measurement didn't accurately + # capture the entire expression (this is necessary for nested + # groups). + should_remeasure = true + else + buffer << doc.separator + position += doc.width + next + end + end + + # If there are any commands in the line suffix buffer, then we're going + # to flush them now, as we are about to add a newline. + if line_suffixes.any? + commands << [indent, mode, doc] + commands += line_suffixes.reverse + line_suffixes = [] + next + end + + if !doc.indent? + buffer << newline + + if indent.root + buffer << indent.root.value + position = indent.root.length + else + position = 0 + end + else + position -= buffer.trim! + buffer << newline + buffer << indent.value + position = indent.length + end + when BreakParent + # do nothing + else + # Special case where the user has defined some way to get an extra doc + # node that we don't explicitly support into the list. In this case + # we're going to assume it's 0-width and just append it to the output + # buffer. + # + # This is useful behavior for putting marker nodes into the list so that + # you can know how things are getting mapped before they get printed. + buffer << doc + end + + if commands.empty? && line_suffixes.any? + commands += line_suffixes.reverse + line_suffixes = [] + end + end + + # Reset the group stack and target array so that this pretty printer object + # can continue to be used before calling flush again if desired. + reset + end + + # ---------------------------------------------------------------------------- + # Markers node builders + # ---------------------------------------------------------------------------- + + # This says "you can break a line here if necessary", and a +width+\-column + # text +separator+ is inserted if a line is not broken at the point. + # + # If +separator+ is not specified, ' ' is used. + # + # If +width+ is not specified, +separator.length+ is used. You will have to + # specify this when +separator+ is a multibyte character, for example. + # + # By default, if the surrounding group is broken and a newline is inserted, + # the printer will indent the subsequent line up to the current level of + # indentation. You can disable this behavior with the +indent+ argument if + # that's not desired (rare). + # + # By default, when you insert a Breakable into the print tree, it only breaks + # the surrounding group when the group's contents cannot fit onto the + # remaining space of the current line. You can force it to break the + # surrounding group instead if you always want the newline with the +force+ + # argument. + def breakable( + separator = " ", + width = separator.length, + indent: true, + force: false + ) + doc = Breakable.new(separator, width, indent: indent, force: force) + + target << doc + break_parent if force + + doc + end + + # This inserts a BreakParent node into the print tree which forces the + # surrounding and all parent group nodes to break. + def break_parent + doc = BreakParent.new + target << doc + + groups.reverse_each do |group| + break if group.break? + group.break + end + + doc + end + + # This is similar to #breakable except the decision to break or not is + # determined individually. + # + # Two #fill_breakable under a group may cause 4 results: + # (break,break), (break,non-break), (non-break,break), (non-break,non-break). + # This is different to #breakable because two #breakable under a group + # may cause 2 results: (break,break), (non-break,non-break). + # + # The text +separator+ is inserted if a line is not broken at this point. + # + # If +separator+ is not specified, ' ' is used. + # + # If +width+ is not specified, +separator.length+ is used. You will have to + # specify this when +separator+ is a multibyte character, for example. + def fill_breakable(separator = " ", width = separator.length) + group { breakable(separator, width) } + end + + # This inserts a Trim node into the print tree which, when printed, will clear + # all whitespace at the end of the output buffer. This is useful for the rare + # case where you need to delete printed indentation and force the next node + # to start at the beginning of the line. + def trim + doc = Trim.new + target << doc + + doc + end + + # ---------------------------------------------------------------------------- + # Container node builders + # ---------------------------------------------------------------------------- + + # Groups line break hints added in the block. The line break hints are all to + # be used or not. + # + # If +indent+ is specified, the method call is regarded as nested by + # nest(indent) { ... }. + # + # If +open_object+ is specified, text(open_object, open_width) is + # called before grouping. If +close_object+ is specified, + # text(close_object, close_width) is called after grouping. + def group( + indent = 0, + open_object = "", + close_object = "", + open_width = open_object.length, + close_width = close_object.length + ) + text(open_object, open_width) if open_object != "" + + doc = Group.new(groups.last.depth + 1) + groups << doc + target << doc + + with_target(doc.contents) do + if indent != 0 + nest(indent) { yield } + else + yield + end + end + + groups.pop + text(close_object, close_width) if close_object != "" + + doc + end + + # A small DSL-like object used for specifying the alternative contents to be + # printed if the surrounding group doesn't break for an IfBreak node. + class IfBreakBuilder + attr_reader :builder, :if_break + + def initialize(builder, if_break) + @builder = builder + @if_break = if_break + end + + def if_flat(&block) + builder.with_target(if_break.flat_contents, &block) + end + end + + # Inserts an IfBreak node with the contents of the block being added to its + # list of nodes that should be printed if the surrounding node breaks. If it + # doesn't, then you can specify the contents to be printed with the #if_flat + # method used on the return object from this method. For example, + # + # q.if_break { q.text('do') }.if_flat { q.text('{') } + # + # In the example above, if the surrounding group is broken it will print 'do' + # and if it is not it will print '{'. + def if_break + doc = IfBreak.new + target << doc + + with_target(doc.break_contents) { yield } + IfBreakBuilder.new(self, doc) + end + + # Very similar to the #nest method, this indents the nested content by one + # level by inserting an Indent node into the print tree. The contents of the + # node are determined by the block. + def indent + doc = Indent.new + target << doc + + with_target(doc.contents) { yield } + doc + end + + # Inserts a LineSuffix node into the print tree. The contents of the node are + # determined by the block. + def line_suffix + doc = LineSuffix.new + target << doc + + with_target(doc.contents) { yield } + doc + end + + # Increases left margin after newline with +indent+ for line breaks added in + # the block. + def nest(indent) + doc = Align.new(indent: indent) + target << doc + + with_target(doc.contents) { yield } + doc + end + + # This adds +object+ as a text of +width+ columns in width. + # + # If +width+ is not specified, object.length is used. + def text(object = "", width = object.length) + doc = target.last + + unless Text === doc + doc = Text.new + target << doc + end + + doc.add(object: object, width: width) + doc + end + + # ---------------------------------------------------------------------------- + # Internal APIs + # ---------------------------------------------------------------------------- + + # A convenience method used by a lot of the print tree node builders that + # temporarily changes the target that the builders will append to. + def with_target(target) + previous_target, @target = @target, target + yield + @target = previous_target + end + + private + + # This method returns a boolean as to whether or not the remaining commands + # fit onto the remaining space on the current line. If we finish printing + # all of the commands or if we hit a newline, then we return true. Otherwise + # if we continue printing past the remaining space, we return false. + def fits?(next_command, rest_commands, remaining) + # This is the index in the remaining commands that we've handled so far. + # We reverse through the commands and add them to the stack if we've run + # out of nodes to handle. + rest_index = rest_commands.length + + # This is our stack of commands, very similar to the commands list in the + # print method. + commands = [next_command] + + # This is our output buffer, really only necessary to keep track of + # because we could encounter a Trim doc node that would actually add + # remaining space. + fit_buffer = buffer.class.new + + while remaining >= 0 + if commands.empty? + return true if rest_index == 0 + + rest_index -= 1 + commands << rest_commands[rest_index] + next + end + + indent, mode, doc = commands.pop + + case doc + when Text + doc.objects.each { |object| fit_buffer << object } + remaining -= doc.width + when Array + doc.reverse_each { |part| commands << [indent, mode, part] } + when Indent + commands << [indent.indent, mode, doc.contents] + when Align + commands << [indent.align(doc.indent), mode, doc.contents] + when Trim + remaining += fit_buffer.trim! + when Group + commands << [indent, doc.break? ? MODE_BREAK : mode, doc.contents] + when IfBreak + if mode == MODE_BREAK + commands << [indent, mode, doc.break_contents] if doc.break_contents + else + commands << [indent, mode, doc.flat_contents] if doc.flat_contents + end + when Breakable + if mode == MODE_FLAT && !doc.force? + fit_buffer << doc.separator + remaining -= doc.width + next + end + + return true + end + end + + false + end + + # Resets the group stack and target array so that this pretty printer object + # can continue to be used before calling flush again if desired. + def reset + @groups = [Group.new(0)] + @target = @groups.last.contents + end +end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb new file mode 100644 index 00000000..4c45315d --- /dev/null +++ b/lib/syntax_tree/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "ripper" + +class SyntaxTree < Ripper + VERSION = "0.1.0" +end diff --git a/ripper-parse_tree.gemspec b/syntax_tree.gemspec similarity index 73% rename from ripper-parse_tree.gemspec rename to syntax_tree.gemspec index 7a3f91f5..d17296f6 100644 --- a/ripper-parse_tree.gemspec +++ b/syntax_tree.gemspec @@ -1,16 +1,17 @@ # frozen_string_literal: true -require_relative 'lib/ripper/parse_tree/version' +require_relative 'lib/syntax_tree/version' Gem::Specification.new do |spec| - spec.name = 'template' - spec.version = Ripper::ParseTree::VERSION + spec.name = 'syntax_tree' + spec.version = SyntaxTree::VERSION spec.authors = ['Kevin Newton'] spec.email = ['kddnewton@gmail.com'] spec.summary = 'A parser based on ripper' - spec.homepage = 'https://github.com/kddnewton/ripper-parse_tree' + spec.homepage = 'https://github.com/kddnewton/syntax_tree' spec.license = 'MIT' + spec.metadata = { 'rubygems_mfa_required' => 'true' } spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f| diff --git a/test/fixtures/CHAR.rb b/test/fixtures/CHAR.rb new file mode 100644 index 00000000..9ecdc2b9 --- /dev/null +++ b/test/fixtures/CHAR.rb @@ -0,0 +1,14 @@ +% +?a +- +"a" +% +?\C-a +% +?\M-a +% +?\M-\C-a +% +?a # comment +- +"a" # comment diff --git a/test/fixtures/access_ctrl.rb b/test/fixtures/access_ctrl.rb new file mode 100644 index 00000000..1f35fd8a --- /dev/null +++ b/test/fixtures/access_ctrl.rb @@ -0,0 +1,30 @@ +% +class Foo + private +end +% +class Foo + private + def foo + end +end +- +class Foo + private + + def foo + end +end +% +class Foo + def foo + end + private +end +- +class Foo + def foo + end + + private +end diff --git a/test/fixtures/alias.rb b/test/fixtures/alias.rb new file mode 100644 index 00000000..8962cc32 --- /dev/null +++ b/test/fixtures/alias.rb @@ -0,0 +1,33 @@ +% +alias foo bar +% +alias << push +% +alias in within +% +alias in IN +% +alias :foo :bar +- +alias foo bar +% +alias :"foo" :bar +- +alias :"foo" bar +% +alias :foo :"bar" +- +alias foo :"bar" +% +alias foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo bar +- +alias foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + bar +% +alias foo bar # comment +% +alias foo # comment + bar +% +alias foo # comment1 + bar # comment2 diff --git a/test/fixtures/aref.rb b/test/fixtures/aref.rb new file mode 100644 index 00000000..255af549 --- /dev/null +++ b/test/fixtures/aref.rb @@ -0,0 +1,8 @@ +% +foo[bar] +% +foo[] +% +foo[ + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +] diff --git a/test/fixtures/aref_field.rb b/test/fixtures/aref_field.rb new file mode 100644 index 00000000..93f338bd --- /dev/null +++ b/test/fixtures/aref_field.rb @@ -0,0 +1,10 @@ +% +foo[bar] = baz +% +foo[] = baz +% +foo[ + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +] = baz +% +foo[bar] # comment diff --git a/test/fixtures/arg_block.rb b/test/fixtures/arg_block.rb new file mode 100644 index 00000000..74be5b2b --- /dev/null +++ b/test/fixtures/arg_block.rb @@ -0,0 +1,16 @@ +% +foo(&bar) +% +foo( + &bar +) +- +foo(&bar) +% +foo(&bar.baz) +% +foo(&bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) +- +foo( + &bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +) diff --git a/test/fixtures/arg_paren.rb b/test/fixtures/arg_paren.rb new file mode 100644 index 00000000..0816af6a --- /dev/null +++ b/test/fixtures/arg_paren.rb @@ -0,0 +1,16 @@ +% +foo(bar) +% +foo() +% +foo(barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr) +- +foo( + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +) +% +foo( + bar +) +- +foo(bar) diff --git a/test/fixtures/arg_star.rb b/test/fixtures/arg_star.rb new file mode 100644 index 00000000..06be0133 --- /dev/null +++ b/test/fixtures/arg_star.rb @@ -0,0 +1,16 @@ +% +foo(*bar) +% +foo( + *bar +) +- +foo(*bar) +% +foo(*bar.baz) +% +foo(*bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) +- +foo( + *bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +) diff --git a/test/fixtures/args.rb b/test/fixtures/args.rb new file mode 100644 index 00000000..47a69700 --- /dev/null +++ b/test/fixtures/args.rb @@ -0,0 +1,21 @@ +% +foo(bar, baz) +% +foo(barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz) +- +foo( + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, + baz +) +% +foo( + bar, + baz +) +- +foo(bar, baz) +% +foo( + bar, # comment + baz +) diff --git a/test/fixtures/args_forward.rb b/test/fixtures/args_forward.rb new file mode 100644 index 00000000..e38a22cc --- /dev/null +++ b/test/fixtures/args_forward.rb @@ -0,0 +1,4 @@ +% +def get(...) + request(:GET, ...) +end diff --git a/test/fixtures/array_literal.rb b/test/fixtures/array_literal.rb new file mode 100644 index 00000000..8402b7db --- /dev/null +++ b/test/fixtures/array_literal.rb @@ -0,0 +1,66 @@ +% +[] +% +[foo, bar, baz] +% +[foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, bar, baz] +- +[ + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, + bar, + baz +] +% +[ + foo, + bar, + baz +] +- +[foo, bar, baz] +% +["foo"] +% +["foo", "bar"] +- +%w[foo bar] +% +[ + "foo", + "bar" # comment +] +% +["foo", "bar"] # comment +- +%w[foo bar] # comment +% +["foo", :bar] +% +["foo", "#{bar}"] +% +["foo", " bar "] +% +["foo", "bar\n"] +% +["foo", "bar]"] +% +[:foo] +% +[:foo, :bar] +- +%i[foo bar] +% +[ + :foo, + :bar # comment +] +% +[:foo, :bar] # comment +- +%i[foo bar] # comment +% +[:foo, "bar"] +% +[:foo, :"bar"] +% +[foo, bar] # comment diff --git a/test/fixtures/aryptn.rb b/test/fixtures/aryptn.rb new file mode 100644 index 00000000..19f1ab13 --- /dev/null +++ b/test/fixtures/aryptn.rb @@ -0,0 +1,52 @@ +% +case foo +in _, _ +end +% +case foo +in bar, baz +end +% +case foo +in [bar] +end +% +case foo +in [bar, baz] +end +- +case foo +in bar, baz +end +% +case foo +in bar, *baz +end +% +case foo +in *bar, baz +end +% +case foo +in bar, *, baz +end +% +case foo +in *, bar, baz +end +% +case foo +in Constant[bar] +end +% +case foo +in Constant[bar, baz] +end +% +case foo +in bar, [baz, _] => qux +end +% +case foo +in bar, baz if bar == baz +end diff --git a/test/fixtures/assign.rb b/test/fixtures/assign.rb new file mode 100644 index 00000000..eb0ceefd --- /dev/null +++ b/test/fixtures/assign.rb @@ -0,0 +1,32 @@ +% +foo = bar +% +foo = + begin + bar + end +% +foo = <<~HERE + bar +HERE +% +foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +- +foo = + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +% +foo = [barrrrrrrrrrrrrrrrrrrrr, barrrrrrrrrrrrrrrrrrrrr, barrrrrrrrrrrrrrrrrrrrr] +- +foo = [ + barrrrrrrrrrrrrrrrrrrrr, + barrrrrrrrrrrrrrrrrrrrr, + barrrrrrrrrrrrrrrrrrrrr +] +% +foo = { bar1: bazzzzzzzzzzzzzzz, bar2: bazzzzzzzzzzzzzzz, bar3: bazzzzzzzzzzzzzzz } +- +foo = { + bar1: bazzzzzzzzzzzzzzz, + bar2: bazzzzzzzzzzzzzzz, + bar3: bazzzzzzzzzzzzzzz +} diff --git a/test/fixtures/assoc.rb b/test/fixtures/assoc.rb new file mode 100644 index 00000000..293f4e26 --- /dev/null +++ b/test/fixtures/assoc.rb @@ -0,0 +1,16 @@ +% +{ foo: bar } +% +{ foo: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } +- +{ + foo: + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +} +% +{ + foo: + bar +} +- +{ foo: bar } diff --git a/test/fixtures/assoc_splat.rb b/test/fixtures/assoc_splat.rb new file mode 100644 index 00000000..2182c2ed --- /dev/null +++ b/test/fixtures/assoc_splat.rb @@ -0,0 +1,14 @@ +% +{ **foo } +% +{ **foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } +- +{ + **foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +} +% +{ + **foo +} +- +{ **foo } diff --git a/test/fixtures/backref.rb b/test/fixtures/backref.rb new file mode 100644 index 00000000..9f03eed0 --- /dev/null +++ b/test/fixtures/backref.rb @@ -0,0 +1,4 @@ +% +$1 +% +$1 # comment diff --git a/test/fixtures/backtick.rb b/test/fixtures/backtick.rb new file mode 100644 index 00000000..08a81a0c --- /dev/null +++ b/test/fixtures/backtick.rb @@ -0,0 +1,3 @@ +% +def `(value) +end diff --git a/test/fixtures/bare_assoc_hash.rb b/test/fixtures/bare_assoc_hash.rb new file mode 100644 index 00000000..d9114eec --- /dev/null +++ b/test/fixtures/bare_assoc_hash.rb @@ -0,0 +1,25 @@ +% +foo(bar: bar) +% +foo(:bar => bar) +- +foo(bar: bar) +% +foo(:"bar" => bar) +- +foo("bar": bar) +% +foo(bar => bar, baz: baz) +- +foo(bar => bar, :baz => baz) +% +foo(bar => bar, "baz": baz) +- +foo(bar => bar, :"baz" => baz) +% +foo(bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) +- +foo( + bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, + baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +) diff --git a/test/fixtures/begin.rb b/test/fixtures/begin.rb new file mode 100644 index 00000000..efd12dad --- /dev/null +++ b/test/fixtures/begin.rb @@ -0,0 +1,7 @@ +% +begin +end +% +begin + expression +end diff --git a/test/fixtures/begin_block.rb b/test/fixtures/begin_block.rb new file mode 100644 index 00000000..8a27cfe0 --- /dev/null +++ b/test/fixtures/begin_block.rb @@ -0,0 +1,27 @@ +% +BEGIN { foo } +% +BEGIN { + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +} +% +BEGIN { + foo +} +- +BEGIN { foo } +% +BEGIN { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } +- +BEGIN { + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +} +% +BEGIN { # comment + foo +} +% +BEGIN { + # comment + foo +} diff --git a/test/fixtures/binary.rb b/test/fixtures/binary.rb new file mode 100644 index 00000000..f8833cdc --- /dev/null +++ b/test/fixtures/binary.rb @@ -0,0 +1,11 @@ +% +foo + bar +% +foo << bar +% +foo**bar +% +foo * barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +- +foo * + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr diff --git a/test/fixtures/block_arg.rb b/test/fixtures/block_arg.rb new file mode 100644 index 00000000..f00be759 --- /dev/null +++ b/test/fixtures/block_arg.rb @@ -0,0 +1,3 @@ +% +def foo(&bar) +end diff --git a/test/fixtures/block_var.rb b/test/fixtures/block_var.rb new file mode 100644 index 00000000..db72ca5e --- /dev/null +++ b/test/fixtures/block_var.rb @@ -0,0 +1,6 @@ +% +foo { |bar, baz| } +% +foo { |bar; baz| } +% +foo { |bar, baz; qux, qaz| } diff --git a/test/fixtures/bodystmt.rb b/test/fixtures/bodystmt.rb new file mode 100644 index 00000000..120255a8 --- /dev/null +++ b/test/fixtures/bodystmt.rb @@ -0,0 +1,36 @@ +% +begin + foo +rescue Foo + foo +rescue Bar + foo +else + foo +ensure + foo +end +% +begin + foo +rescue Foo + foo +rescue Bar + foo +end +% +begin + foo +rescue Foo + foo +rescue Bar + foo +else + foo +end +% +begin + foo +ensure + foo +end diff --git a/test/fixtures/brace_block.rb b/test/fixtures/brace_block.rb new file mode 100644 index 00000000..9242f2e0 --- /dev/null +++ b/test/fixtures/brace_block.rb @@ -0,0 +1,8 @@ +% +foo {} +% +foo { # comment +} +- +foo do # comment +end diff --git a/test/fixtures/break.rb b/test/fixtures/break.rb new file mode 100644 index 00000000..9193a1cd --- /dev/null +++ b/test/fixtures/break.rb @@ -0,0 +1,23 @@ +% +break +% +break foo +% +break foo, bar +% +break(foo) +% +break fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +% +break(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +break( + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) +% +break (foo), bar +% +break( + foo + bar +) diff --git a/test/fixtures/case.rb b/test/fixtures/case.rb new file mode 100644 index 00000000..72415407 --- /dev/null +++ b/test/fixtures/case.rb @@ -0,0 +1,10 @@ +% +case foo +when bar + baz +end +% +case +when bar + baz +end diff --git a/test/fixtures/class.rb b/test/fixtures/class.rb new file mode 100644 index 00000000..8316f22a --- /dev/null +++ b/test/fixtures/class.rb @@ -0,0 +1,38 @@ +% +class Foo +end +% +class Foo + foo +end +% +class Foo + # comment +end +% +class Foo # comment +end +% +module Foo + class Bar + end +end +% +class Foo < foo +end +% +class Foo < foo + foo +end +% +class Foo < foo + # comment +end +% +class Foo < foo # comment +end +% +module Foo + class Bar < foo + end +end diff --git a/test/fixtures/command.rb b/test/fixtures/command.rb new file mode 100644 index 00000000..d74ae8ab --- /dev/null +++ b/test/fixtures/command.rb @@ -0,0 +1,7 @@ +% +foo bar +% +foo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +- +foo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, + bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz diff --git a/test/fixtures/command_call.rb b/test/fixtures/command_call.rb new file mode 100644 index 00000000..9ad0a3ac --- /dev/null +++ b/test/fixtures/command_call.rb @@ -0,0 +1,7 @@ +% +foo.bar baz +% +foo.bar barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +- +foo.bar barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, + bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz diff --git a/test/fixtures/const.rb b/test/fixtures/const.rb new file mode 100644 index 00000000..2c026f0c --- /dev/null +++ b/test/fixtures/const.rb @@ -0,0 +1,4 @@ +% +Foo +% +Foo # comment diff --git a/test/fixtures/const_path_field.rb b/test/fixtures/const_path_field.rb new file mode 100644 index 00000000..15cf9faf --- /dev/null +++ b/test/fixtures/const_path_field.rb @@ -0,0 +1,2 @@ +% +foo::Bar = baz diff --git a/test/fixtures/const_path_ref.rb b/test/fixtures/const_path_ref.rb new file mode 100644 index 00000000..d3a3c63e --- /dev/null +++ b/test/fixtures/const_path_ref.rb @@ -0,0 +1,2 @@ +% +foo::Bar diff --git a/test/fixtures/const_ref.rb b/test/fixtures/const_ref.rb new file mode 100644 index 00000000..1b750469 --- /dev/null +++ b/test/fixtures/const_ref.rb @@ -0,0 +1,6 @@ +% +class Foo +end +% +class Foo::Bar +end diff --git a/test/fixtures/cvar.rb b/test/fixtures/cvar.rb new file mode 100644 index 00000000..a2aab71d --- /dev/null +++ b/test/fixtures/cvar.rb @@ -0,0 +1,4 @@ +% +@@foo +% +@@foo # comment diff --git a/test/fixtures/def.rb b/test/fixtures/def.rb new file mode 100644 index 00000000..a827adfe --- /dev/null +++ b/test/fixtures/def.rb @@ -0,0 +1,25 @@ +% +def foo(bar) + baz +end +% +def foo bar + baz +end +- +def foo(bar) + baz +end +% +def foo(bar) # comment +end +% +def foo() +end +% +def foo() # comment +end +% +def foo( # comment +) +end diff --git a/test/fixtures/def_endless.rb b/test/fixtures/def_endless.rb new file mode 100644 index 00000000..2f316e6c --- /dev/null +++ b/test/fixtures/def_endless.rb @@ -0,0 +1,8 @@ +% +def foo = bar +% +def foo(bar) = baz +% +def foo() = bar +- +def foo = bar diff --git a/test/fixtures/defined.rb b/test/fixtures/defined.rb new file mode 100644 index 00000000..d18aedac --- /dev/null +++ b/test/fixtures/defined.rb @@ -0,0 +1,14 @@ +% +defined?(foo) +% +defined?(foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +defined?( + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) +% +defined?( + foo +) +- +defined?(foo) diff --git a/test/fixtures/defs.rb b/test/fixtures/defs.rb new file mode 100644 index 00000000..03f841ba --- /dev/null +++ b/test/fixtures/defs.rb @@ -0,0 +1,31 @@ +% +def foo.foo(bar) + baz +end +% +def foo.foo bar + baz +end +- +def foo.foo(bar) + baz +end +% +def foo.foo(bar) # comment +end +% +def foo.foo() +end +% +def foo.foo() # comment +end +% +def foo.foo( # comment +) +end +% +def foo::foo +end +- +def foo.foo +end diff --git a/test/fixtures/do_block.rb b/test/fixtures/do_block.rb new file mode 100644 index 00000000..60de0e12 --- /dev/null +++ b/test/fixtures/do_block.rb @@ -0,0 +1,12 @@ +% +foo do +end +- +foo {} +% +foo do + # comment +end +% +foo do # comment +end diff --git a/test/fixtures/dot2.rb b/test/fixtures/dot2.rb new file mode 100644 index 00000000..cbca6f58 --- /dev/null +++ b/test/fixtures/dot2.rb @@ -0,0 +1,18 @@ +% +foo..bar +% +foo.. +% +..bar +% +foo..bar # comment +% +foo.. # comment +% +..bar # comment +% +if foo == bar .. foo == baz +end +% +unless foo == bar .. foo == baz +end diff --git a/test/fixtures/dot3.rb b/test/fixtures/dot3.rb new file mode 100644 index 00000000..410ccb72 --- /dev/null +++ b/test/fixtures/dot3.rb @@ -0,0 +1,18 @@ +% +foo...bar +% +foo... +% +...bar +% +foo...bar # comment +% +foo... # comment +% +...bar # comment +% +if foo == bar ... foo == baz +end +% +unless foo == bar ... foo == baz +end diff --git a/test/fixtures/dyna_symbol.rb b/test/fixtures/dyna_symbol.rb new file mode 100644 index 00000000..63a277b0 --- /dev/null +++ b/test/fixtures/dyna_symbol.rb @@ -0,0 +1,22 @@ +% +:'foo' +- +:"foo" +% +:"foo" +% +:'foo #{bar}' +% +:"foo #{bar}" +% +%s[foo #{bar}] +- +:'foo #{bar}' +% +{ %s[foo] => bar } +- +{ "foo": bar } +% +%s[ + foo +] diff --git a/test/fixtures/else.rb b/test/fixtures/else.rb new file mode 100644 index 00000000..d3675c27 --- /dev/null +++ b/test/fixtures/else.rb @@ -0,0 +1,20 @@ +% +case +when foo +else +end +% +if foo +else +end +% +case +when foo +else + bar +end +% +if foo +else + bar +end diff --git a/test/fixtures/elsif.rb b/test/fixtures/elsif.rb new file mode 100644 index 00000000..2e4cd831 --- /dev/null +++ b/test/fixtures/elsif.rb @@ -0,0 +1,19 @@ +% +if foo + bar +elsif baz +end +% +if foo + bar +elsif baz + qux +end +% +if foo + bar +elsif baz + qux +else + qyz +end diff --git a/test/fixtures/embdoc.rb b/test/fixtures/embdoc.rb new file mode 100644 index 00000000..134a42e7 --- /dev/null +++ b/test/fixtures/embdoc.rb @@ -0,0 +1,10 @@ +% +=begin +comment +=end +% +module Foo +=begin +comment +=end +end diff --git a/test/fixtures/end_block.rb b/test/fixtures/end_block.rb new file mode 100644 index 00000000..2f481f56 --- /dev/null +++ b/test/fixtures/end_block.rb @@ -0,0 +1,27 @@ +% +END { foo } +% +END { + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +} +% +END { + foo +} +- +END { foo } +% +END { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } +- +END { + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +} +% +END { # comment + foo +} +% +END { + # comment + foo +} diff --git a/test/fixtures/end_content.rb b/test/fixtures/end_content.rb new file mode 100644 index 00000000..5699d74c --- /dev/null +++ b/test/fixtures/end_content.rb @@ -0,0 +1,9 @@ +% +foo = bar + +__END__ + /‾‾‾‾‾\ /‾/ /‾/ /‾‾‾‾‾\ |‾| /‾/ + / /‾‾/ / / / / / / /‾‾/ / | |/ / + / ‾‾‾ / / / / / / ‾‾‾_/ | / + / /‾\ \‾ / /_/ / / /‾‾/ | / / +|_/ /_/ |_____/ |__‾_‾_/ |__/ diff --git a/test/fixtures/ensure.rb b/test/fixtures/ensure.rb new file mode 100644 index 00000000..cb1b56c5 --- /dev/null +++ b/test/fixtures/ensure.rb @@ -0,0 +1,14 @@ +% +begin +ensure +end +% +begin +ensure + foo +end +% +begin +ensure + # comment +end diff --git a/test/fixtures/excessed_comma.rb b/test/fixtures/excessed_comma.rb new file mode 100644 index 00000000..c3c742d4 --- /dev/null +++ b/test/fixtures/excessed_comma.rb @@ -0,0 +1,4 @@ +% +foo.each do |bar, baz,| + # comment +end diff --git a/test/fixtures/fcall.rb b/test/fixtures/fcall.rb new file mode 100644 index 00000000..7c3ae5e0 --- /dev/null +++ b/test/fixtures/fcall.rb @@ -0,0 +1,2 @@ +% +foo(bar) diff --git a/test/fixtures/field.rb b/test/fixtures/field.rb new file mode 100644 index 00000000..c0b51065 --- /dev/null +++ b/test/fixtures/field.rb @@ -0,0 +1,2 @@ +% +foo.bar = baz diff --git a/test/fixtures/float_literal.rb b/test/fixtures/float_literal.rb new file mode 100644 index 00000000..3ab75806 --- /dev/null +++ b/test/fixtures/float_literal.rb @@ -0,0 +1,4 @@ +% +1.0 +% +1.0 # comment diff --git a/test/fixtures/fndptn.rb b/test/fixtures/fndptn.rb new file mode 100644 index 00000000..815912c4 --- /dev/null +++ b/test/fixtures/fndptn.rb @@ -0,0 +1,40 @@ +% +case foo +in [*, bar, *] +end +% +case foo +in [*, bar, baz, qux, *] +end +% +case foo +in [*foo, bar, *] +end +% +case foo +in [*, bar, *baz] +end +% +case foo +in [*foo, bar, *baz] +end +% +case foo +in Foo[*, bar, *] +end +% +case foo +in Foo[*, bar, baz, qux, *] +end +% +case foo +in Foo[*foo, bar, *] +end +% +case foo +in Foo[*, bar, *baz] +end +% +case foo +in Foo[*foo, bar, *baz] +end diff --git a/test/fixtures/for.rb b/test/fixtures/for.rb new file mode 100644 index 00000000..c1a848e6 --- /dev/null +++ b/test/fixtures/for.rb @@ -0,0 +1,36 @@ +% +for foo in bar +end +% +for foo in bar + foo +end +% +for foo in bar + # comment +end +% +for foo, bar, baz in bar +end +% +for foo, bar, baz in bar + foo +end +% +for foo, bar, baz in bar + # comment +end +% +foo do + # comment + for bar in baz do + bar + end +end +- +foo do + # comment + for bar in baz + bar + end +end diff --git a/test/fixtures/gvar.rb b/test/fixtures/gvar.rb new file mode 100644 index 00000000..f5c89a71 --- /dev/null +++ b/test/fixtures/gvar.rb @@ -0,0 +1,4 @@ +% +$foo +% +$foo # comment diff --git a/test/fixtures/hash.rb b/test/fixtures/hash.rb new file mode 100644 index 00000000..757f7bca --- /dev/null +++ b/test/fixtures/hash.rb @@ -0,0 +1,25 @@ +% +{ bar: bar } +% +{ :bar => bar } +- +{ bar: bar } +% +{ :"bar" => bar } +- +{ "bar": bar } +% +{ bar => bar, baz: baz } +- +{ bar => bar, :baz => baz } +% +{ bar => bar, "baz": baz } +- +{ bar => bar, :"baz" => baz } +% +{ bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz } +- +{ + bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, + baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +} diff --git a/test/fixtures/heredoc.rb b/test/fixtures/heredoc.rb new file mode 100644 index 00000000..8dad2ad3 --- /dev/null +++ b/test/fixtures/heredoc.rb @@ -0,0 +1,111 @@ +% +<<-FOO + bar +FOO +% +<<-FOO + bar + #{baz} +FOO +% +<<-FOO + foo + #{<<-BAR} + bar +BAR +FOO +% +<<-FOO + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + #{foo} + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +FOO +% +def foo + <<~FOO.strip + foo + FOO +end +% +<<~FOO + bar +FOO +% +<<~FOO + bar + #{baz} +FOO +% +<<~FOO + foo + #{<<~BAR} + bar + BAR +FOO +% +<<~FOO + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + #{foo} + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +FOO +% +def foo + <<~FOO.strip + foo + FOO +end +% +call(foo, bar, baz, <<~FOO) + foo +FOO +% +call(foo, bar, baz, <<~FOO, <<~BAR) + foo +FOO + bar +BAR +% +command foo, bar, baz, <<~FOO + foo +FOO +% +command foo, bar, baz, <<~FOO, <<~BAR + foo +FOO + bar +BAR +% +command.call foo, bar, baz, <<~FOO + foo +FOO +% +command.call foo, bar, baz, <<~FOO, <<~BAR + foo +FOO + bar +BAR +% +foo = <<~FOO.strip + foo +FOO +% +foo( + <<~FOO, + foo + FOO + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo: + :bar +) +% +foo(<<~FOO + foo +FOO +) { "foo" } +- +foo(<<~FOO) { "foo" } + foo +FOO diff --git a/test/fixtures/heredoc_beg.rb b/test/fixtures/heredoc_beg.rb new file mode 100644 index 00000000..3474064c --- /dev/null +++ b/test/fixtures/heredoc_beg.rb @@ -0,0 +1,18 @@ +% +<<-FOO +FOO +% +<<~FOO +FOO +% +<<-`FOO` +FOO +% +<<-FOO.strip +FOO +% +<<~FOO.strip +FOO +% +<<-`FOO`.strip +FOO diff --git a/test/fixtures/hshptn.rb b/test/fixtures/hshptn.rb new file mode 100644 index 00000000..2efe2fd3 --- /dev/null +++ b/test/fixtures/hshptn.rb @@ -0,0 +1,46 @@ +% +case foo +in bar: +end +% +case foo +in bar: bar +end +% +case foo +in bar:, baz: +end +% +case foo +in bar: bar, baz: baz +end +% +case foo +in **bar +end +% +case foo +in foo:, # comment1 + bar: # comment2 + baz +end +% +case foo +in Foo[bar:] +end +% +case foo +in Foo[bar: bar] +end +% +case foo +in Foo[bar:, baz:] +end +% +case foo +in Foo[bar: bar, baz: baz] +end +% +case foo +in Foo[**bar] +end diff --git a/test/fixtures/ident.rb b/test/fixtures/ident.rb new file mode 100644 index 00000000..e9c14ee9 --- /dev/null +++ b/test/fixtures/ident.rb @@ -0,0 +1,2 @@ +% +foo diff --git a/test/fixtures/if.rb b/test/fixtures/if.rb new file mode 100644 index 00000000..2e013a2c --- /dev/null +++ b/test/fixtures/if.rb @@ -0,0 +1,18 @@ +% +if foo +end +% +if foo +else +end +% +if foo + bar +end +- +bar if foo +% +if foo + bar +else +end diff --git a/test/fixtures/if_mod.rb b/test/fixtures/if_mod.rb new file mode 100644 index 00000000..04e529a8 --- /dev/null +++ b/test/fixtures/if_mod.rb @@ -0,0 +1,10 @@ +% +bar if foo +% +barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr if foo +- +if foo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +end +% +bar if foo # comment diff --git a/test/fixtures/ifop.rb b/test/fixtures/ifop.rb new file mode 100644 index 00000000..b37dc5b0 --- /dev/null +++ b/test/fixtures/ifop.rb @@ -0,0 +1,10 @@ +% +foo ? bar : baz +% +foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ? bar : baz +- +if foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + bar +else + baz +end diff --git a/test/fixtures/imaginary.rb b/test/fixtures/imaginary.rb new file mode 100644 index 00000000..9880cfa1 --- /dev/null +++ b/test/fixtures/imaginary.rb @@ -0,0 +1,4 @@ +% +1i +% +1i # comment diff --git a/test/fixtures/in.rb b/test/fixtures/in.rb new file mode 100644 index 00000000..1e1b2282 --- /dev/null +++ b/test/fixtures/in.rb @@ -0,0 +1,34 @@ +% +case foo +in foo +end +% +case foo +in foo + baz +end +% +case foo +in fooooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr + baz +end +- +case foo +in fooooooooooooooooooooooooooooooooooooo, + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr + baz +end +% +case foo +in foo +in bar +end +% +case foo +in bar + # comment +end +% +case foo +in bar if baz +end diff --git a/test/fixtures/int.rb b/test/fixtures/int.rb new file mode 100644 index 00000000..0921f7cd --- /dev/null +++ b/test/fixtures/int.rb @@ -0,0 +1,12 @@ +% +1 +% +1 # comment +% +12345 +- +12_345 +% +2020_01_01 +% +0b11111 diff --git a/test/fixtures/ivar.rb b/test/fixtures/ivar.rb new file mode 100644 index 00000000..da624d1b --- /dev/null +++ b/test/fixtures/ivar.rb @@ -0,0 +1,4 @@ +% +@foo +% +@foo # comment diff --git a/test/fixtures/kw.rb b/test/fixtures/kw.rb new file mode 100644 index 00000000..10959114 --- /dev/null +++ b/test/fixtures/kw.rb @@ -0,0 +1,5 @@ +% +:if +% +def if +end diff --git a/test/fixtures/kwrest_param.rb b/test/fixtures/kwrest_param.rb new file mode 100644 index 00000000..e56957b8 --- /dev/null +++ b/test/fixtures/kwrest_param.rb @@ -0,0 +1,16 @@ +% +def foo(**bar) +end +% +def foo(**) +end +% +def foo( + **bar # comment +) +end +% +def foo( + ** # comment +) +end diff --git a/test/fixtures/label.rb b/test/fixtures/label.rb new file mode 100644 index 00000000..14de6874 --- /dev/null +++ b/test/fixtures/label.rb @@ -0,0 +1,6 @@ +% +{ foo: bar } +% +case foo +in bar: +end diff --git a/test/fixtures/lambda.rb b/test/fixtures/lambda.rb new file mode 100644 index 00000000..601c6d69 --- /dev/null +++ b/test/fixtures/lambda.rb @@ -0,0 +1,38 @@ +% +-> { foo } +% +->(foo, bar) { baz } +% +-> foo { bar } +- +->(foo) { bar } +% +-> () { foo } +- +-> { foo } +% +-> { fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } +- +-> do + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +end +% +->(foo) { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } +- +->(foo) do + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +end +% +command foo, ->(bar) { bar } +% +command foo, ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } +- +command foo, + ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } +% +command.call foo, ->(bar) { bar } +% +command.call foo, ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } +- +command.call foo, + ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } diff --git a/test/fixtures/massign.rb b/test/fixtures/massign.rb new file mode 100644 index 00000000..83da7aa2 --- /dev/null +++ b/test/fixtures/massign.rb @@ -0,0 +1,4 @@ +% +foo, bar = baz, qux +% +foo, bar, = baz, qux diff --git a/test/fixtures/method_add_arg.rb b/test/fixtures/method_add_arg.rb new file mode 100644 index 00000000..2078855f --- /dev/null +++ b/test/fixtures/method_add_arg.rb @@ -0,0 +1,8 @@ +% +foo(bar) +% +foo.bar(baz) +% +foo.() +% +foo? diff --git a/test/fixtures/method_add_block.rb b/test/fixtures/method_add_block.rb new file mode 100644 index 00000000..cfa5cea0 --- /dev/null +++ b/test/fixtures/method_add_block.rb @@ -0,0 +1,2 @@ +% +foo {} diff --git a/test/fixtures/mlhs.rb b/test/fixtures/mlhs.rb new file mode 100644 index 00000000..50a7dee1 --- /dev/null +++ b/test/fixtures/mlhs.rb @@ -0,0 +1,10 @@ +% +foo, bar = baz +% +foo, bar, = baz +% +foo, *bar, baz = baz +% +foo, *bar, baz = baz +% +foo1, foo2, *bar, baz1, baz2 = baz diff --git a/test/fixtures/mlhs_paren.rb b/test/fixtures/mlhs_paren.rb new file mode 100644 index 00000000..dee89c3b --- /dev/null +++ b/test/fixtures/mlhs_paren.rb @@ -0,0 +1,10 @@ +% +(foo, bar) = baz +- +foo, bar = baz +% +foo, (bar, baz) = baz +% +(foo, bar), baz = baz +% +foo, (bar, baz,) = baz diff --git a/test/fixtures/module.rb b/test/fixtures/module.rb new file mode 100644 index 00000000..221abbc6 --- /dev/null +++ b/test/fixtures/module.rb @@ -0,0 +1,19 @@ +% +module Foo +end +% +module Foo + foo +end +% +module Foo + # comment +end +% +module Foo # comment +end +% +module Foo + module Bar + end +end diff --git a/test/fixtures/mrhs.rb b/test/fixtures/mrhs.rb new file mode 100644 index 00000000..23049f1e --- /dev/null +++ b/test/fixtures/mrhs.rb @@ -0,0 +1,8 @@ +% +foo = bar, baz +% +foo = bar, *baz, qux +% +foo = *bar, baz +% +foo = bar, *baz diff --git a/test/fixtures/next.rb b/test/fixtures/next.rb new file mode 100644 index 00000000..13947746 --- /dev/null +++ b/test/fixtures/next.rb @@ -0,0 +1,23 @@ +% +next +% +next foo +% +next foo, bar +% +next(foo) +% +next fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +% +next(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +next( + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) +% +next (foo), bar +% +next( + foo + bar +) diff --git a/test/fixtures/not.rb b/test/fixtures/not.rb new file mode 100644 index 00000000..eaa456f1 --- /dev/null +++ b/test/fixtures/not.rb @@ -0,0 +1,4 @@ +% +not foo +% +not(foo) diff --git a/test/fixtures/op.rb b/test/fixtures/op.rb new file mode 100644 index 00000000..6002c9ed --- /dev/null +++ b/test/fixtures/op.rb @@ -0,0 +1,3 @@ +% +def +(other) +end diff --git a/test/fixtures/opassign.rb b/test/fixtures/opassign.rb new file mode 100644 index 00000000..28017754 --- /dev/null +++ b/test/fixtures/opassign.rb @@ -0,0 +1,5 @@ +% +foo += bar +% +foo += # comment + bar diff --git a/test/fixtures/params.rb b/test/fixtures/params.rb new file mode 100644 index 00000000..67b6ec90 --- /dev/null +++ b/test/fixtures/params.rb @@ -0,0 +1,88 @@ +% +def foo(req) +end +% +def foo(req1, req2) +end +% +def foo(optl = foo) +end +% +def foo(optl1 = foo, optl2 = bar) +end +% +def foo(*) +end +% +def foo(*rest) +end +% +def foo(...) +end +% +def foo(*, post) +end +% +def foo(*, post1, post2) +end +% +def foo(key:) +end +% +def foo(key1:, key2:) +end +% +def foo(key: foo) +end +% +def foo(key1: foo, key2: bar) +end +% +def foo(**) +end +% +def foo(**kwrest) +end +% +def foo(&block) +end +% +def foo(req1, req2, optl = foo, *rest, key1:, key2: bar, **kwrest, &block) +end +% +foo { |req| } +% +foo { |req1, req2| } +% +foo { |optl = foo| } +% +foo { |optl1 = foo, optl2 = bar| } +% +foo { |*| } +% +foo { |*rest| } +% +foo { |req,| } +% +foo { |*, post| } +% +foo { |*, post1, post2| } +% +foo { |key:| } +% +foo { |key1:, key2:| } +% +foo { |key: foo| } +% +foo { |key1: foo, key2: bar| } +% +foo { |**| } +% +foo { |**kwrest| } +% +foo { |&block| } +% +foo { |req1, req2, optl = foo, *rest, key1:, key2: bar, **kwrest, &block| } +% +foo do |foooooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrr| +end diff --git a/test/fixtures/paren.rb b/test/fixtures/paren.rb new file mode 100644 index 00000000..ec94019b --- /dev/null +++ b/test/fixtures/paren.rb @@ -0,0 +1,9 @@ +% +(foo + bar) +% +( + foo + bar +) +% +(foo) diff --git a/test/fixtures/period.rb b/test/fixtures/period.rb new file mode 100644 index 00000000..9f11763c --- /dev/null +++ b/test/fixtures/period.rb @@ -0,0 +1,2 @@ +% +foo.bar diff --git a/test/fixtures/program.rb b/test/fixtures/program.rb new file mode 100644 index 00000000..e9c14ee9 --- /dev/null +++ b/test/fixtures/program.rb @@ -0,0 +1,2 @@ +% +foo diff --git a/test/fixtures/qsymbols.rb b/test/fixtures/qsymbols.rb new file mode 100644 index 00000000..c9ebe9b4 --- /dev/null +++ b/test/fixtures/qsymbols.rb @@ -0,0 +1,17 @@ +% +%i[foo bar] +% +%i[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] +- +%i[ + fooooooooooooooooooooooooooooooooooooo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +] +% +%i[ + foo +] +- +%i[foo] +% +%i[foo] # comment diff --git a/test/fixtures/qwords.rb b/test/fixtures/qwords.rb new file mode 100644 index 00000000..14b25be6 --- /dev/null +++ b/test/fixtures/qwords.rb @@ -0,0 +1,17 @@ +% +%w[foo bar] +% +%w[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] +- +%w[ + fooooooooooooooooooooooooooooooooooooo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +] +% +%w[ + foo +] +- +%w[foo] +% +%w[foo] # comment diff --git a/test/fixtures/rassign.rb b/test/fixtures/rassign.rb new file mode 100644 index 00000000..882ce890 --- /dev/null +++ b/test/fixtures/rassign.rb @@ -0,0 +1,14 @@ +% +foo in bar +% +foo => bar +% +foooooooooooooooooooooooooooooooooooooo in barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +- +foooooooooooooooooooooooooooooooooooooo in + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +% +foooooooooooooooooooooooooooooooooooooo => barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +- +foooooooooooooooooooooooooooooooooooooo => + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr diff --git a/test/fixtures/rational_literal.rb b/test/fixtures/rational_literal.rb new file mode 100644 index 00000000..32c496eb --- /dev/null +++ b/test/fixtures/rational_literal.rb @@ -0,0 +1,4 @@ +% +1r +% +1r # comment diff --git a/test/fixtures/redo.rb b/test/fixtures/redo.rb new file mode 100644 index 00000000..8ab087a2 --- /dev/null +++ b/test/fixtures/redo.rb @@ -0,0 +1,4 @@ +% +redo +% +redo # comment diff --git a/test/fixtures/regexp_literal.rb b/test/fixtures/regexp_literal.rb new file mode 100644 index 00000000..8ae0a03d --- /dev/null +++ b/test/fixtures/regexp_literal.rb @@ -0,0 +1,51 @@ +% +/foo/ +% +%r{foo} +- +/foo/ +% +%r/foo/ +- +/foo/ +% +%r[foo] +- +/foo/ +% +%r(foo) +- +/foo/ +% +%r{foo/bar/baz} +% +/foo #{bar} baz/ +% +/foo/i +% +%r{foo/bar/baz}mi +% +/#$&/ +- +/#{$&}/ +% +%r(a{b/c}) +% +%r[a}b/c] +% +%r(a}bc) +- +/a}bc/ +% +/\\A + [[:digit:]]+ # 1 or more digits before the decimal point + (\\. # Decimal point + [[:digit:]]+ # 1 or more digits after the decimal point + )? # The decimal point and following digits are optional +\\Z/x +% +foo %r{ bar} +% +foo %r{= bar} +% +foo(/ bar/) diff --git a/test/fixtures/rescue.rb b/test/fixtures/rescue.rb new file mode 100644 index 00000000..68603b15 --- /dev/null +++ b/test/fixtures/rescue.rb @@ -0,0 +1,56 @@ +% +begin +rescue +end +- +begin +rescue StandardError +end +% +begin +rescue => foo + bar +end +% +begin +rescue Foo + bar +end +% +begin +rescue Foo => foo + bar +end +% +begin +rescue Foo, Bar +end +% +begin +rescue Foo, *Bar +end +% +begin +rescue Foo, Bar => foo +end +% +begin +rescue Foo, *Bar => foo +end +% # https://github.com/prettier/plugin-ruby/pull/1000 +begin +rescue ::Foo +end +% +begin +rescue Foo +rescue Bar +end +% +begin +rescue Foo # comment +end +% +begin +rescue Foo, *Bar # comment +end diff --git a/test/fixtures/rescue_mod.rb b/test/fixtures/rescue_mod.rb new file mode 100644 index 00000000..6e0c131e --- /dev/null +++ b/test/fixtures/rescue_mod.rb @@ -0,0 +1,8 @@ +% +bar rescue foo +- +begin + bar +rescue StandardError + foo +end diff --git a/test/fixtures/rest_param.rb b/test/fixtures/rest_param.rb new file mode 100644 index 00000000..1471a8f3 --- /dev/null +++ b/test/fixtures/rest_param.rb @@ -0,0 +1,16 @@ +% +def foo(*bar) +end +% +def foo(*) +end +% +def foo( + *bar # comment +) +end +% +def foo( + * # comment +) +end diff --git a/test/fixtures/retry.rb b/test/fixtures/retry.rb new file mode 100644 index 00000000..2b14d21a --- /dev/null +++ b/test/fixtures/retry.rb @@ -0,0 +1,4 @@ +% +retry +% +retry # comment diff --git a/test/fixtures/return.rb b/test/fixtures/return.rb new file mode 100644 index 00000000..390e5e2f --- /dev/null +++ b/test/fixtures/return.rb @@ -0,0 +1,23 @@ +% +return +% +return foo +% +return foo, bar +% +return(foo) +% +return fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +% +return(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +return( + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) +% +return (foo), bar +% +return( + foo + bar +) diff --git a/test/fixtures/return0.rb b/test/fixtures/return0.rb new file mode 100644 index 00000000..7654c934 --- /dev/null +++ b/test/fixtures/return0.rb @@ -0,0 +1,4 @@ +% +return +% +return # comment diff --git a/test/fixtures/sclass.rb b/test/fixtures/sclass.rb new file mode 100644 index 00000000..7d89a0e2 --- /dev/null +++ b/test/fixtures/sclass.rb @@ -0,0 +1,21 @@ +% +class << self + foo +end +% +class << foo + bar +end +% +class << self # comment + foo +end +% +class << self + # comment +end +% +class << self + # comment1 + # comment2 +end diff --git a/test/fixtures/statements.rb b/test/fixtures/statements.rb new file mode 100644 index 00000000..6331808e --- /dev/null +++ b/test/fixtures/statements.rb @@ -0,0 +1,24 @@ +% +# comment1 +# comment2 +% +foo do + # comment1 + # comment2 +end +% +foo + + +bar +- +foo + +bar +% +foo; bar +- +foo +bar +% +"#{foo; bar}" diff --git a/test/fixtures/string_concat.rb b/test/fixtures/string_concat.rb new file mode 100644 index 00000000..7bb55baf --- /dev/null +++ b/test/fixtures/string_concat.rb @@ -0,0 +1,4 @@ +% +"foo" \ + "bar" \ + "baz" diff --git a/test/fixtures/string_dvar.rb b/test/fixtures/string_dvar.rb new file mode 100644 index 00000000..fc1efc76 --- /dev/null +++ b/test/fixtures/string_dvar.rb @@ -0,0 +1,8 @@ +% +"#@foo" +- +"#{@foo}" +% +"#@foo" # comment +- +"#{@foo}" # comment diff --git a/test/fixtures/string_embexpr.rb b/test/fixtures/string_embexpr.rb new file mode 100644 index 00000000..fd5e8cfc --- /dev/null +++ b/test/fixtures/string_embexpr.rb @@ -0,0 +1,12 @@ +% +"foo #{bar}" +% +"foo #{super}" +% +"#{bar} foo" +% +"foo #{"bar #{baz} bar"} foo" +% +"#{foo; bar}" +% +"#{if foo; foooooooooooooooooooooooooooooooooooooo; else; barrrrrrrrrrrrrrrr; end}" diff --git a/test/fixtures/string_literal.rb b/test/fixtures/string_literal.rb new file mode 100644 index 00000000..ebe56a40 --- /dev/null +++ b/test/fixtures/string_literal.rb @@ -0,0 +1,44 @@ +% +%(foo \\ bar) +% +%[foo \\ bar] +% +%{foo \\ bar} +% +% +% +%|foo \\ bar| +% +%q(foo \\ bar) +% +%q[foo \\ bar] +% +%q{foo \\ bar} +% +%q +% +%q|foo \\ bar| +% +%Q(foo \\ bar) +% +%Q[foo \\ bar] +% +%Q{foo \\ bar} +% +%Q +% +%Q|foo \\ bar| +% +'' +- +"" +% +'foo' +- +"foo" +% +'foo #{bar}' +% +'"foo"' +- +"\"foo\"" diff --git a/test/fixtures/super.rb b/test/fixtures/super.rb new file mode 100644 index 00000000..3fc43fb0 --- /dev/null +++ b/test/fixtures/super.rb @@ -0,0 +1,28 @@ +% +super() +% +super foo +% +super(foo) +% +super foo, bar +% +super(foo, bar) +% +super() # comment +% +super foo # comment +% +super(foo) # comment +% +super foo, bar # comment +% +super(foo, bar) # comment +% +super foo, # comment1 + bar # comment2 +% +super( + foo, # comment1 + bar # comment2 +) diff --git a/test/fixtures/symbol_literal.rb b/test/fixtures/symbol_literal.rb new file mode 100644 index 00000000..26029b7b --- /dev/null +++ b/test/fixtures/symbol_literal.rb @@ -0,0 +1,4 @@ +% +:foo +% +:foo # comment diff --git a/test/fixtures/symbols.rb b/test/fixtures/symbols.rb new file mode 100644 index 00000000..c67d6555 --- /dev/null +++ b/test/fixtures/symbols.rb @@ -0,0 +1,19 @@ +% +%I[foo bar] +% +%I[foo #{bar}] +% +%I[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] +- +%I[ + fooooooooooooooooooooooooooooooooooooo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +] +% +%I[ + foo +] +- +%I[foo] +% +%I[foo] # comment diff --git a/test/fixtures/top_const_field.rb b/test/fixtures/top_const_field.rb new file mode 100644 index 00000000..c00ad9a9 --- /dev/null +++ b/test/fixtures/top_const_field.rb @@ -0,0 +1,2 @@ +% +::Foo::Bar = baz diff --git a/test/fixtures/top_const_ref.rb b/test/fixtures/top_const_ref.rb new file mode 100644 index 00000000..bbc38580 --- /dev/null +++ b/test/fixtures/top_const_ref.rb @@ -0,0 +1,2 @@ +% +::Foo::Bar diff --git a/test/fixtures/tstring_content.rb b/test/fixtures/tstring_content.rb new file mode 100644 index 00000000..bac5b1e1 --- /dev/null +++ b/test/fixtures/tstring_content.rb @@ -0,0 +1,2 @@ +% +"foo" diff --git a/test/fixtures/unary.rb b/test/fixtures/unary.rb new file mode 100644 index 00000000..f801de62 --- /dev/null +++ b/test/fixtures/unary.rb @@ -0,0 +1,4 @@ +% +!foo +% # https://github.com/prettier/plugin-ruby/issues/764 +!(foo&.>(0)) diff --git a/test/fixtures/undef.rb b/test/fixtures/undef.rb new file mode 100644 index 00000000..de42d5c3 --- /dev/null +++ b/test/fixtures/undef.rb @@ -0,0 +1,23 @@ +% +undef foo +% +undef foo, bar +% +undef foooooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +- +undef foooooooooooooooooooooooooooooooooooooo, + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +% +undef foo # comment +% +undef foo, # comment + bar +% +undef foo, # comment1 + bar, # comment2 + baz +% +undef foo, + bar # comment +- +undef foo, bar # comment diff --git a/test/fixtures/unless.rb b/test/fixtures/unless.rb new file mode 100644 index 00000000..3041f849 --- /dev/null +++ b/test/fixtures/unless.rb @@ -0,0 +1,18 @@ +% +unless foo +end +% +unless foo +else +end +% +unless foo + bar +end +- +bar unless foo +% +unless foo + bar +else +end diff --git a/test/fixtures/unless_mod.rb b/test/fixtures/unless_mod.rb new file mode 100644 index 00000000..30b73f37 --- /dev/null +++ b/test/fixtures/unless_mod.rb @@ -0,0 +1,10 @@ +% +bar unless foo +% +barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr unless foo +- +unless foo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +end +% +bar unless foo # comment diff --git a/test/fixtures/until.rb b/test/fixtures/until.rb new file mode 100644 index 00000000..0daa09ac --- /dev/null +++ b/test/fixtures/until.rb @@ -0,0 +1,13 @@ +% +until foo +end +% +until foo + bar +end +- +bar until foo +% +until fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + bar +end diff --git a/test/fixtures/until_mod.rb b/test/fixtures/until_mod.rb new file mode 100644 index 00000000..f3bc7ce3 --- /dev/null +++ b/test/fixtures/until_mod.rb @@ -0,0 +1,10 @@ +% +bar until foo +% +barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr until foo +- +until foo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +end +% +bar until foo # comment diff --git a/test/fixtures/var_alias.rb b/test/fixtures/var_alias.rb new file mode 100644 index 00000000..092e857c --- /dev/null +++ b/test/fixtures/var_alias.rb @@ -0,0 +1,8 @@ +% +alias $1 $foo +% +alias $foo $bar +% +alias $1 $foo # comment +% +alias $foo $bar # comment diff --git a/test/fixtures/var_field.rb b/test/fixtures/var_field.rb new file mode 100644 index 00000000..8c1258af --- /dev/null +++ b/test/fixtures/var_field.rb @@ -0,0 +1,10 @@ +% +Foo = bar +% +@@foo = bar +% +$foo = bar +% +foo = bar +% +@foo = bar diff --git a/test/fixtures/var_ref.rb b/test/fixtures/var_ref.rb new file mode 100644 index 00000000..57dfa1c8 --- /dev/null +++ b/test/fixtures/var_ref.rb @@ -0,0 +1,18 @@ +% +Foo +% +@@foo +% +$foo +% +foo +% +@foo +% +self +% +true +% +false +% +nil diff --git a/test/fixtures/vcall.rb b/test/fixtures/vcall.rb new file mode 100644 index 00000000..ff74ccb3 --- /dev/null +++ b/test/fixtures/vcall.rb @@ -0,0 +1,4 @@ +% +foo +% +foo # comment diff --git a/test/fixtures/void_stmt.rb b/test/fixtures/void_stmt.rb new file mode 100644 index 00000000..126d857b --- /dev/null +++ b/test/fixtures/void_stmt.rb @@ -0,0 +1,4 @@ +% +;;; +- + diff --git a/test/fixtures/when.rb b/test/fixtures/when.rb new file mode 100644 index 00000000..22ebdd1d --- /dev/null +++ b/test/fixtures/when.rb @@ -0,0 +1,48 @@ +% +case +when foo +end +% +case +when foo, bar + baz +end +% +case +when foooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr + baz +end +- +case +when foooooooooooooooooooooooooooooooooooo, + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr + baz +end +% +case +when foo then bar +end +- +case +when foo + bar +end +% +case +when foooooooooooooooooo, barrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +end +- +case +when foooooooooooooooooo, barrrrrrrrrrrrrrrrrr, + bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +end +% +case +when foo +when bar +end +% +case +when foo +else +end diff --git a/test/fixtures/while.rb b/test/fixtures/while.rb new file mode 100644 index 00000000..d8a79f89 --- /dev/null +++ b/test/fixtures/while.rb @@ -0,0 +1,13 @@ +% +while foo +end +% +while foo + bar +end +- +bar while foo +% +while fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + bar +end diff --git a/test/fixtures/while_mod.rb b/test/fixtures/while_mod.rb new file mode 100644 index 00000000..482f3934 --- /dev/null +++ b/test/fixtures/while_mod.rb @@ -0,0 +1,10 @@ +% +bar while foo +% +barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr while foo +- +while foo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +end +% +bar while foo # comment diff --git a/test/fixtures/word.rb b/test/fixtures/word.rb new file mode 100644 index 00000000..731bb32c --- /dev/null +++ b/test/fixtures/word.rb @@ -0,0 +1,6 @@ +% +%W[foo] +% +%W[foo\ bar] +% +%W[foo#{bar}baz] diff --git a/test/fixtures/words.rb b/test/fixtures/words.rb new file mode 100644 index 00000000..021eac7a --- /dev/null +++ b/test/fixtures/words.rb @@ -0,0 +1,19 @@ +% +%W[foo bar] +% +%W[foo #{bar}] +% +%W[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] +- +%W[ + fooooooooooooooooooooooooooooooooooooo + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +] +% +%W[ + foo +] +- +%W[foo] +% +%W[foo] # comment diff --git a/test/fixtures/xstring_literal.rb b/test/fixtures/xstring_literal.rb new file mode 100644 index 00000000..0d47a59e --- /dev/null +++ b/test/fixtures/xstring_literal.rb @@ -0,0 +1,20 @@ +% +`foo` +% +`foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo` +% +`foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo`.to_s +% +%x[foo] +- +`foo` +% +%x[foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo] +- +`foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo` +% +%x[foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo].to_s +- +`foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo`.to_s +% +`foo` # comment diff --git a/test/fixtures/yield.rb b/test/fixtures/yield.rb new file mode 100644 index 00000000..f3f023f8 --- /dev/null +++ b/test/fixtures/yield.rb @@ -0,0 +1,16 @@ +% +yield foo +% +yield(foo) +% +yield foo, bar +% +yield(foo, bar) +% +yield foo # comment +% +yield(foo) # comment +% +yield( # comment + foo +) diff --git a/test/fixtures/yield0.rb b/test/fixtures/yield0.rb new file mode 100644 index 00000000..a168c4aa --- /dev/null +++ b/test/fixtures/yield0.rb @@ -0,0 +1,4 @@ +% +yield +% +yield # comment diff --git a/test/fixtures/zsuper.rb b/test/fixtures/zsuper.rb new file mode 100644 index 00000000..839d20b6 --- /dev/null +++ b/test/fixtures/zsuper.rb @@ -0,0 +1,4 @@ +% +super +% +super # comment diff --git a/test/parse_tree_test.rb b/test/parse_tree_test.rb deleted file mode 100644 index dcc3b3d5..00000000 --- a/test/parse_tree_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class ParseTreeTest < Minitest::Test - def test_version - refute_nil Ripper::ParseTree::VERSION - end -end diff --git a/test/syntax_tree_test.rb b/test/syntax_tree_test.rb new file mode 100644 index 00000000..ea706066 --- /dev/null +++ b/test/syntax_tree_test.rb @@ -0,0 +1,1066 @@ +# frozen_string_literal: true + +require "simplecov" +SimpleCov.start { add_filter("prettyprint.rb") } + +$LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) +require "syntax_tree" + +require "json" +require "pp" +require "minitest/autorun" + +class SyntaxTree + class SyntaxTreeTest < Minitest::Test + # -------------------------------------------------------------------------- + # Tests for behavior + # -------------------------------------------------------------------------- + + def test_multibyte + assign = SyntaxTree.parse("🎉 + 🎉").statements.body.first + assert_equal(5, assign.location.end_char) + end + + def test_parse_error + assert_raises(ParseError) { SyntaxTree.parse("<>") } + end + + def test_next_statement_start + source = <<~SOURCE + def method # comment + expression + end + SOURCE + + bodystmt = SyntaxTree.parse(source).statements.body.first.bodystmt + assert_equal(20, bodystmt.location.start_char) + end + + def test_version + refute_nil(VERSION) + end + + # -------------------------------------------------------------------------- + # Tests for nodes + # -------------------------------------------------------------------------- + + def test_BEGIN + assert_node(BEGINBlock, "BEGIN", "BEGIN {}") + end + + def test_CHAR + assert_node(CHAR, "CHAR", "?a") + end + + def test_END + assert_node(ENDBlock, "END", "END {}") + end + + def test___end__ + source = <<~SOURCE + a + 1 + __END__ + content + SOURCE + + at = location(lines: 2..2, chars: 6..14) + assert_node(EndContent, "__end__", source, at: at) + end + + def test_alias + assert_node(Alias, "alias", "alias left right") + end + + def test_aref + assert_node(ARef, "aref", "collection[index]") + end + + def test_aref_field + source = "collection[index] = value" + + at = location(chars: 0..17) + assert_node(ARefField, "aref_field", source, at: at, &:target) + end + + def test_arg_paren + source = "method(argument)" + + at = location(chars: 6..16) + assert_node(ArgParen, "arg_paren", source, at: at, &:arguments) + end + + def test_arg_paren_heredoc + source = <<~SOURCE + method(<<~ARGUMENT) + value + ARGUMENT + SOURCE + + at = location(lines: 1..3, chars: 6..28) + assert_node(ArgParen, "arg_paren", source, at: at, &:arguments) + end + + def test_args + source = "method(first, second, third)" + + at = location(chars: 7..27) + assert_node(Args, "args", source, at: at) do |node| + node.arguments.arguments + end + end + + def test_arg_block + source = "method(argument, &block)" + + at = location(chars: 17..23) + assert_node(ArgBlock, "arg_block", source, at: at) do |node| + node.arguments.arguments.parts[1] + end + end + + def test_arg_star + source = "method(prefix, *arguments, suffix)" + + at = location(chars: 15..25) + assert_node(ArgStar, "arg_star", source, at: at) do |node| + node.arguments.arguments.parts[1] + end + end + + def test_args_forward + source = <<~SOURCE + def get(...) + request(:GET, ...) + end + SOURCE + + at = location(lines: 2..2, chars: 29..32) + assert_node(ArgsForward, "args_forward", source, at: at) do |node| + node.bodystmt.statements.body.first.arguments.arguments.parts.last + end + end + + def test_array + assert_node(ArrayLiteral, "array", "[1]") + end + + def test_aryptn + source = <<~SOURCE + case [1, 2, 3] + in Container[Integer, *, Integer] + 'matched' + end + SOURCE + + at = location(lines: 2..2, chars: 18..47) + assert_node(AryPtn, "aryptn", source, at: at) do |node| + node.consequent.pattern + end + end + + def test_assign + assert_node(Assign, "assign", "variable = value") + end + + def test_assoc + source = "{ key1: value1, key2: value2 }" + + at = location(chars: 2..14) + assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first } + end + + def test_assoc_splat + source = "{ **pairs }" + + at = location(chars: 2..9) + assert_node(AssocSplat, "assoc_splat", source, at: at) do |node| + node.assocs.first + end + end + + def test_backref + assert_node(Backref, "backref", "$1") + end + + def test_backtick + at = location(chars: 4..5) + assert_node(Backtick, "backtick", "def `() end", at: at, &:name) + end + + def test_bare_assoc_hash + source = "method(key1: value1, key2: value2)" + + at = location(chars: 7..33) + assert_node(BareAssocHash, "bare_assoc_hash", source, at: at) do |node| + node.arguments.arguments.parts.first + end + end + + def test_begin + source = <<~SOURCE + begin + value + end + SOURCE + + assert_node(Begin, "begin", source) + end + + def test_begin_clauses + source = <<~SOURCE + begin + begun + rescue + rescued + else + elsed + ensure + ensured + end + SOURCE + + assert_node(Begin, "begin", source) + end + + def test_binary + assert_node(Binary, "binary", "collection << value") + end + + def test_block_var + source = <<~SOURCE + method do |positional, optional = value, keyword:, █ local| + end + SOURCE + + at = location(chars: 10..65) + assert_node(BlockVar, "block_var", source, at: at) do |node| + node.block.block_var + end + end + + def test_blockarg + source = "def method(&block); end" + + at = location(chars: 11..17) + assert_node(BlockArg, "blockarg", source, at: at) do |node| + node.params.contents.block + end + end + + def test_bodystmt + source = <<~SOURCE + begin + begun + rescue + rescued + else + elsed + ensure + ensured + end + SOURCE + + at = location(lines: 9..9, chars: 5..64) + assert_node(BodyStmt, "bodystmt", source, at: at, &:bodystmt) + end + + def test_brace_block + source = "method { |variable| variable + 1 }" + + at = location(chars: 7..34) + assert_node(BraceBlock, "brace_block", source, at: at, &:block) + end + + def test_break + assert_node(Break, "break", "break value") + end + + def test_call + assert_node(Call, "call", "receiver.message") + end + + def test_case + source = <<~SOURCE + case value + when 1 + "one" + end + SOURCE + + assert_node(Case, "case", source) + end + + def test_rassign_in + assert_node(RAssign, "rassign", "value in pattern") + end + + def test_rassign_rocket + assert_node(RAssign, "rassign", "value => pattern") + end + + def test_class + assert_node(ClassDeclaration, "class", "class Child < Parent; end") + end + + def test_command + assert_node(Command, "command", "method argument") + end + + def test_command_call + assert_node(CommandCall, "command_call", "object.method argument") + end + + def test_comment + assert_node(Comment, "comment", "# comment", at: location(chars: 0..8)) + end + + def test_const + assert_node(Const, "const", "Constant", &:value) + end + + def test_const_path_field + source = "object::Const = value" + + at = location(chars: 0..13) + assert_node(ConstPathField, "const_path_field", source, at: at, &:target) + end + + def test_const_path_ref + assert_node(ConstPathRef, "const_path_ref", "object::Const") + end + + def test_const_ref + source = "class Container; end" + + at = location(chars: 6..15) + assert_node(ConstRef, "const_ref", source, at: at, &:constant) + end + + def test_cvar + assert_node(CVar, "cvar", "@@variable", &:value) + end + + def test_def + assert_node(Def, "def", "def method(param) result end") + end + + def test_def_paramless + source = <<~SOURCE + def method + end + SOURCE + + assert_node(Def, "def", source) + end + + def test_def_endless + assert_node(DefEndless, "def_endless", "def method = result") + end + + def test_defined + assert_node(Defined, "defined", "defined?(variable)") + end + + def test_defs + assert_node(Defs, "defs", "def object.method(param) result end") + end + + def test_defs_paramless + source = <<~SOURCE + def object.method + end + SOURCE + + assert_node(Defs, "defs", source) + end + + def test_do_block + source = "method do |variable| variable + 1 end" + + at = location(chars: 7..37) + assert_node(DoBlock, "do_block", source, at: at, &:block) + end + + def test_dot2 + assert_node(Dot2, "dot2", "1..3") + end + + def test_dot3 + assert_node(Dot3, "dot3", "1...3") + end + + def test_dyna_symbol + assert_node(DynaSymbol, "dyna_symbol", ':"#{variable}"') + end + + def test_dyna_symbol_hash_key + source = '{ "#{key}": value }' + + at = location(chars: 2..11) + assert_node(DynaSymbol, "dyna_symbol", source, at: at) do |node| + node.assocs.first.key + end + end + + def test_else + source = <<~SOURCE + if value + else + end + SOURCE + + at = location(lines: 2..3, chars: 9..17) + assert_node(Else, "else", source, at: at, &:consequent) + end + + def test_elsif + source = <<~SOURCE + if first + elsif second + else + end + SOURCE + + at = location(lines: 2..4, chars: 9..30) + assert_node(Elsif, "elsif", source, at: at, &:consequent) + end + + def test_embdoc + source = <<~SOURCE + =begin + first line + second line + =end + SOURCE + + assert_node(EmbDoc, "embdoc", source) + end + + def test_ensure + source = <<~SOURCE + begin + ensure + end + SOURCE + + at = location(lines: 2..3, chars: 6..16) + assert_node(Ensure, "ensure", source, at: at) do |node| + node.bodystmt.ensure_clause + end + end + + def test_excessed_comma + source = "proc { |x,| }" + + at = location(chars: 9..10) + assert_node(ExcessedComma, "excessed_comma", source, at: at) do |node| + node.block.block_var.params.rest + end + end + + def test_fcall + source = "method(argument)" + + at = location(chars: 0..6) + assert_node(FCall, "fcall", source, at: at, &:call) + end + + def test_field + source = "object.variable = value" + + at = location(chars: 0..15) + assert_node(Field, "field", source, at: at, &:target) + end + + def test_float_literal + assert_node(FloatLiteral, "float", "1.0") + end + + def test_fndptn + source = <<~SOURCE + case value + in Container[*, 7, *] + end + SOURCE + + at = location(lines: 2..2, chars: 14..32) + assert_node(FndPtn, "fndptn", source, at: at) do |node| + node.consequent.pattern + end + end + + def test_for + assert_node(For, "for", "for value in list do end") + end + + def test_gvar + assert_node(GVar, "gvar", "$variable", &:value) + end + + def test_hash + assert_node(HashLiteral, "hash", "{ key => value }") + end + + def test_heredoc + source = <<~SOURCE + <<~HEREDOC + contents + HEREDOC + SOURCE + + at = location(lines: 1..3, chars: 0..22) + assert_node(Heredoc, "heredoc", source, at: at) + end + + def test_heredoc_beg + source = <<~SOURCE + <<~HEREDOC + contents + HEREDOC + SOURCE + + at = location(chars: 0..11) + assert_node(HeredocBeg, "heredoc_beg", source, at: at, &:beginning) + end + + def test_hshptn + source = <<~SOURCE + case value + in Container[key:, **keys] + end + SOURCE + + at = location(lines: 2..2, chars: 14..36) + assert_node(HshPtn, "hshptn", source, at: at) do |node| + node.consequent.pattern + end + end + + def test_ident + assert_node(Ident, "ident", "value", &:value) + end + + def test_if + assert_node(If, "if", "if value then else end") + end + + def test_ifop + assert_node(IfOp, "ifop", "value ? true : false") + end + + def test_if_mod + assert_node(IfMod, "if_mod", "expression if predicate") + end + + def test_imaginary + assert_node(Imaginary, "imaginary", "1i") + end + + def test_in + source = <<~SOURCE + case value + in first + in second + end + SOURCE + + at = location(lines: 2..4, chars: 11..33) + assert_node(In, "in", source, at: at, &:consequent) + end + + def test_int + assert_node(Int, "int", "1") + end + + def test_ivar + assert_node(IVar, "ivar", "@variable", &:value) + end + + def test_kw + at = location(chars: 1..3) + assert_node(Kw, "kw", ":if", at: at, &:value) + end + + def test_kwrest_param + source = "def method(**kwargs) end" + + at = location(chars: 11..19) + assert_node(KwRestParam, "kwrest_param", source, at: at) do |node| + node.params.contents.keyword_rest + end + end + + def test_label + source = "{ key: value }" + + at = location(chars: 2..6) + assert_node(Label, "label", source, at: at) do |node| + node.assocs.first.key + end + end + + def test_lambda + source = "->(value) { value * 2 }" + + assert_node(Lambda, "lambda", source) + end + + def test_lambda_do + source = "->(value) do value * 2 end" + + assert_node(Lambda, "lambda", source) + end + + def test_lbrace + source = "method {}" + + at = location(chars: 7..8) + assert_node(LBrace, "lbrace", source, at: at) { |node| node.block.lbrace } + end + + def test_lparen + source = "(1 + 1)" + + at = location(chars: 0..1) + assert_node(LParen, "lparen", source, at: at, &:lparen) + end + + def test_massign + assert_node(MAssign, "massign", "first, second, third = value") + end + + def test_method_add_arg + assert_node(MethodAddArg, "method_add_arg", "method(argument)") + end + + def test_method_add_block + assert_node(MethodAddBlock, "method_add_block", "method {}") + end + + def test_mlhs + source = "left, right = value" + + at = location(chars: 0..11) + assert_node(MLHS, "mlhs", source, at: at, &:target) + end + + def test_mlhs_add_post + source = "left, *middle, right = values" + + at = location(chars: 0..20) + assert_node(MLHS, "mlhs", source, at: at, &:target) + end + + def test_mlhs_paren + source = "(left, right) = value" + + at = location(chars: 0..13) + assert_node(MLHSParen, "mlhs_paren", source, at: at, &:target) + end + + def test_module + source = <<~SOURCE + module Container + end + SOURCE + + assert_node(ModuleDeclaration, "module", source) + end + + def test_mrhs + source = "values = first, second, third" + + at = location(chars: 9..29) + assert_node(MRHS, "mrhs", source, at: at, &:value) + end + + def test_mrhs_add_star + source = "values = first, *rest" + + at = location(chars: 9..21) + assert_node(MRHS, "mrhs", source, at: at, &:value) + end + + def test_next + assert_node(Next, "next", "next(value)") + end + + def test_op + at = location(chars: 4..5) + assert_node(Op, "op", "def +(value) end", at: at, &:name) + end + + def test_opassign + assert_node(OpAssign, "opassign", "variable += value") + end + + def test_params + source = <<~SOURCE + def method( + one, two, + three = 3, four = 4, + *five, + six:, seven: 7, + **eight, + &nine + ) end + SOURCE + + at = location(lines: 2..7, chars: 11..93) + assert_node(Params, "params", source, at: at) do |node| + node.params.contents + end + end + + def test_params_posts + source = "def method(*rest, post) end" + + at = location(chars: 11..22) + assert_node(Params, "params", source, at: at) do |node| + node.params.contents + end + end + + def test_paren + assert_node(Paren, "paren", "(1 + 2)") + end + + def test_period + at = location(chars: 6..7) + assert_node(Period, "period", "object.method", at: at, &:operator) + end + + def test_program + parser = SyntaxTree.new("variable") + program = parser.parse + refute(parser.error?) + + json = JSON.parse(program.to_json) + io = StringIO.new + PP.singleline_pp(program, io) + + assert_kind_of(Program, program) + assert_equal(location(chars: 0..8), program.location) + assert_equal("program", json["type"]) + assert_match(/^\(program.*\)$/, io.string) + end + + def test_qsymbols + assert_node(QSymbols, "qsymbols", "%i[one two three]") + end + + def test_qwords + assert_node(QWords, "qwords", "%w[one two three]") + end + + def test_rational + assert_node(RationalLiteral, "rational", "1r") + end + + def test_redo + assert_node(Redo, "redo", "redo") + end + + def test_regexp_literal + assert_node(RegexpLiteral, "regexp_literal", "/abc/") + end + + def test_rescue_ex + source = <<~SOURCE + begin + rescue Exception => exception + end + SOURCE + + at = location(lines: 2..2, chars: 13..35) + assert_node(RescueEx, "rescue_ex", source, at: at) do |node| + node.bodystmt.rescue_clause.exception + end + end + + def test_rescue + source = <<~SOURCE + begin + rescue First + rescue Second, Third + rescue *Fourth + end + SOURCE + + at = location(lines: 2..5, chars: 6..58) + assert_node(Rescue, "rescue", source, at: at) do |node| + node.bodystmt.rescue_clause + end + end + + def test_rescue_mod + assert_node(RescueMod, "rescue_mod", "expression rescue value") + end + + def test_rest_param + source = "def method(*rest) end" + + at = location(chars: 11..16) + assert_node(RestParam, "rest_param", source, at: at) do |node| + node.params.contents.rest + end + end + + def test_retry + assert_node(Retry, "retry", "retry") + end + + def test_return + assert_node(Return, "return", "return value") + end + + def test_return0 + assert_node(Return0, "return0", "return") + end + + def test_sclass + assert_node(SClass, "sclass", "class << self; end") + end + + def test_statements + at = location(chars: 1..6) + assert_node(Statements, "statements", "(value)", at: at, &:contents) + end + + def test_string_concat + source = <<~SOURCE + 'left' \ + 'right' + SOURCE + + assert_node(StringConcat, "string_concat", source) + end + + def test_string_dvar + at = location(chars: 1..11) + assert_node(StringDVar, "string_dvar", '"#@variable"', at: at) do |node| + node.parts.first + end + end + + def test_string_embexpr + source = '"#{variable}"' + + at = location(chars: 1..12) + assert_node(StringEmbExpr, "string_embexpr", source, at: at) do |node| + node.parts.first + end + end + + def test_string_literal + assert_node(StringLiteral, "string_literal", "\"string\"") + end + + def test_super + assert_node(Super, "super", "super value") + end + + def test_symbol_literal + assert_node(SymbolLiteral, "symbol_literal", ":symbol") + end + + def test_symbols + assert_node(Symbols, "symbols", "%I[one two three]") + end + + def test_top_const_field + source = "::Constant = value" + + at = location(chars: 0..10) + assert_node(TopConstField, "top_const_field", source, at: at, &:target) + end + + def test_top_const_ref + assert_node(TopConstRef, "top_const_ref", "::Constant") + end + + def test_tstring_content + source = "\"string\"" + + at = location(chars: 1..7) + assert_node(TStringContent, "tstring_content", source, at: at) do |node| + node.parts.first + end + end + + def test_not + assert_node(Not, "not", "not(value)") + end + + def test_unary + assert_node(Unary, "unary", "+value") + end + + def test_undef + assert_node(Undef, "undef", "undef value") + end + + def test_unless + assert_node(Unless, "unless", "unless value then else end") + end + + def test_unless_mod + assert_node(UnlessMod, "unless_mod", "expression unless predicate") + end + + def test_until + assert_node(Until, "until", "until value do end") + end + + def test_until_mod + assert_node(UntilMod, "until_mod", "expression until predicate") + end + + def test_var_alias + assert_node(VarAlias, "var_alias", "alias $new $old") + end + + def test_var_field + at = location(chars: 0..8) + assert_node(VarField, "var_field", "variable = value", at: at, &:target) + end + + def test_var_ref + assert_node(VarRef, "var_ref", "true") + end + + def test_access_ctrl + assert_node(AccessCtrl, "access_ctrl", "private") + end + + def test_vcall + assert_node(VCall, "vcall", "variable") + end + + def test_void_stmt + assert_node(VoidStmt, "void_stmt", ";;", at: location(chars: 0..0)) + end + + def test_when + source = <<~SOURCE + case value + when one then :one + when two then :two + end + SOURCE + + at = location(lines: 2..4, chars: 11..52) + assert_node(When, "when", source, at: at, &:consequent) + end + + def test_while + assert_node(While, "while", "while value do end") + end + + def test_while_mod + assert_node(WhileMod, "while_mod", "expression while predicate") + end + + def test_word + at = location(chars: 3..7) + assert_node(Word, "word", "%W[word]", at: at) do |node| + node.elements.first + end + end + + def test_words + assert_node(Words, "words", "%W[one two three]") + end + + def test_xstring_literal + assert_node(XStringLiteral, "xstring_literal", "`ls`") + end + + def test_xstring_heredoc + source = <<~SOURCE + <<~`HEREDOC` + ls + HEREDOC + SOURCE + + at = location(lines: 1..3, chars: 0..18) + assert_node(Heredoc, "heredoc", source, at: at) + end + + def test_yield + assert_node(Yield, "yield", "yield value") + end + + def test_yield0 + assert_node(Yield0, "yield0", "yield") + end + + def test_zsuper + assert_node(ZSuper, "zsuper", "super") + end + + # -------------------------------------------------------------------------- + # Tests for formatting + # -------------------------------------------------------------------------- + + Dir[File.join(__dir__, "fixtures", "*.rb")].each do |filepath| + basename = File.basename(filepath, ".rb") + + File.read(filepath).split(/%(?: #.+?)?\n/).drop( + 1 + ).each_with_index do |source, index| + define_method(:"test_formatting_#{basename}_#{index}") do + original, expected = source.split("-\n") + assert_equal(expected || original, SyntaxTree.format(original)) + end + end + end + + private + + def location(lines: 1..1, chars: 0..0) + Location.new( + start_line: lines.begin, + start_char: chars.begin, + end_line: lines.end, + end_char: chars.end + ) + end + + def assert_node(kind, type, source, at: nil) + at ||= + location( + lines: 1..[1, source.count("\n")].max, + chars: 0..source.chomp.size + ) + + # Parse the example, get the outputted parse tree, and assert that it was + # able to successfully parse. + parser = SyntaxTree.new(source) + program = parser.parse + refute(parser.error?) + + # Grab the last statement out of the parsed output. If a block is given, + # then yield that statement so that the test can descend further down the + # tree to get to the node it is testing. + node = program.statements.body.last + node = yield(node) if block_given? + + # Assert that the found node is the right type and that it has been found + # at the expected location. + assert_kind_of(kind, node) + assert_equal(at, node.location) + + # Serialize the node to JSON, parse it back out, and assert that we have + # found the expected type. + json = JSON.parse(node.to_json) + assert_equal(type, json["type"]) + + # Pretty-print the node to a singleline and then assert that the top + # s-expression of the printed output matches the expected type. + io = StringIO.new + PP.singleline_pp(node, io) + assert_match(/^\(#{type}.*\)$/, io.string) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index 83df8bd0..00000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'simplecov' -SimpleCov.start - -$LOAD_PATH.unshift File.expand_path('../lib', __dir__) -require 'ripper/parse_tree' - -require 'minitest/autorun'