diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ab95d6..973a891c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [1.1.1] - 2021-12-09 + +### Added + +- [#7](https://github.com/kddnewton/syntax_tree/issues/7) Better formatting for hashes and arrays that are values in hashes. +- [#9](https://github.com/kddnewton/syntax_tree/issues/9) Special handling for RSpec matchers when nesting `CommandCall` nodes. +- [#10](https://github.com/kddnewton/syntax_tree/issues/10) Force the maintaining of the modifier forms of conditionals and loops if the statement includes an assignment. Also, for the maintaining of the block form of conditionals and loops if the predicate includes an assignment. + ## [1.1.0] - 2021-12-08 ### Added @@ -89,6 +97,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/kddnewton/syntax_tree/compare/v1.0.0...HEAD +[unreleased]: https://github.com/kddnewton/syntax_tree/compare/v1.1.1...HEAD +[1.1.1]: https://github.com/kddnewton/syntax_tree/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/kddnewton/syntax_tree/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/kddnewton/syntax_tree/compare/v0.1.0...v1.0.0 [0.1.0]: https://github.com/kddnewton/syntax_tree/compare/8aa1f5...v0.1.0 diff --git a/Gemfile.lock b/Gemfile.lock index 0059156b..74c7611b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (1.1.0) + syntax_tree (1.1.1) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index 42d82d42..0103db27 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![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. +A fast Ruby parser and formatter with only standard library dependencies. ## Installation diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index fc05cd83..858b70a3 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -133,8 +133,8 @@ def initialize(source, ...) @quote = "\"" end - def format(node) - stack << node + def format(node, stackable: true) + stack << node if stackable doc = nil # If there are comments, then we're going to format them around the node @@ -168,7 +168,7 @@ def format(node) doc = node.format(self) end - stack.pop + stack.pop if stackable doc end @@ -294,6 +294,19 @@ def self.format(source) output.join end + # Returns the source from the given filepath taking into account any potential + # magic encoding comments. + def self.read(filepath) + encoding = + File.open(filepath, "r") do |file| + header = file.readline + header += file.readline if header.start_with?("#!") + Ripper.new(header).tap(&:parse).encoding + end + + File.read(filepath, encoding: encoding) + end + private # ---------------------------------------------------------------------------- @@ -763,11 +776,11 @@ def format(q) q.group do q.text(keyword) - q.format(left_argument) + q.format(left_argument, stackable: false) q.group do q.nest(keyword.length) do q.breakable(force: left_argument.comments.any?) - q.format(AliasArgumentFormatter.new(right)) + q.format(AliasArgumentFormatter.new(right), stackable: false) end end end @@ -1654,7 +1667,7 @@ def initialize( end def child_nodes - [constant, *required, rest, *posts] + [constant, *requireds, rest, *posts] end def format(q) @@ -1742,6 +1755,23 @@ def on_aryptn(constant, requireds, rest, posts) ) end + # Determins if the following value should be indented or not. + module AssignFormatting + def self.skip_indent?(value) + (value.is_a?(Call) && skip_indent?(value.receiver)) || + [ + ArrayLiteral, + HashLiteral, + Heredoc, + Lambda, + QSymbols, + QWords, + Symbols, + Words + ].include?(value.class) + end + end + # Assign represents assigning something to a variable or constant. Generally, # the left side of the assignment is going to be any node that ends with the # name "Field". @@ -1778,7 +1808,7 @@ def format(q) q.format(target) q.text(" =") - if target.comments.empty? && (skip_indent_target? || skip_indent_value?) + if skip_indent? q.text(" ") q.format(value) else @@ -1816,21 +1846,9 @@ def to_json(*opts) private - def skip_indent_target? - target.is_a?(ARefField) - end - - def skip_indent_value? - [ - ArrayLiteral, - HashLiteral, - Heredoc, - Lambda, - QSymbols, - QWords, - Symbols, - Words - ].any? { |type| value.is_a?(type) } + def skip_indent? + target.comments.empty? && + (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value)) end end @@ -1878,15 +1896,11 @@ def child_nodes end def format(q) - contents = -> do - q.parent.format_key(q, key) - q.indent do - q.breakable - q.format(value) - end + if value.is_a?(HashLiteral) + format_contents(q) + else + q.group { format_contents(q) } end - - value.is_a?(HashLiteral) ? contents.call : q.group(&contents) end def pretty_print(q) @@ -1912,6 +1926,22 @@ def to_json(*opts) cmts: comments }.to_json(*opts) end + + private + + def format_contents(q) + q.parent.format_key(q, key) + + if key.comments.empty? && AssignFormatting.skip_indent?(value) + q.text(" ") + q.format(value) + else + q.indent do + q.breakable + q.format(value) + end + end + end end # :call-seq: @@ -2100,25 +2130,8 @@ def on_backtick(value) # 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 + module HashKeyFormatter + class Labels def format_key(q, key) case key when Label @@ -2133,7 +2146,7 @@ def format_key(q, key) end end - class Rockets < Base + class Rockets def format_key(q, key) case key when Label @@ -2173,7 +2186,7 @@ def self.for(container) end end - (labels ? Labels : Rockets).new(container) + (labels ? Labels : Rockets).new end end @@ -2204,7 +2217,11 @@ def child_nodes end def format(q) - q.format(HashFormatter.for(self)) + q.seplist(assocs) { |assoc| q.format(assoc) } + end + + def format_key(q, key) + (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) end def pretty_print(q) @@ -2887,7 +2904,7 @@ def forced_brace_bounds?(q) def format_break(q, opening, closing) q.text(" ") - q.format(BlockOpenFormatter.new(opening, block_open)) + q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) if node.block_var q.text(" ") @@ -2907,7 +2924,7 @@ def format_break(q, opening, closing) def format_flat(q, opening, closing) q.text(" ") - q.format(BlockOpenFormatter.new(opening, block_open)) + q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) if node.block_var q.breakable @@ -3220,7 +3237,11 @@ def format(q) if receiver.comments.any? || call_operator.comments.any? q.breakable(force: true) end - q.format(call_operator) if call_operator.comments.empty? + + if call_operator.comments.empty? + q.format(call_operator, stackable: false) + end + q.format(message) if message != :call end @@ -3768,19 +3789,13 @@ def format(q) doc = q.nest(0) do q.format(receiver) - q.format(CallOperatorFormatter.new(operator)) + q.format(CallOperatorFormatter.new(operator), stackable: false) q.format(message) end if arguments - width = doc_width(doc) + 1 q.text(" ") - - if width > (q.maxwidth / 2) - q.format(arguments) - else - q.nest(width) { q.format(arguments) } - end + q.nest(argument_alignment(q, doc)) { q.format(arguments) } end end end @@ -3845,6 +3860,28 @@ def doc_width(parent) width end + + def argument_alignment(q, doc) + # Very special handling case for rspec matchers. In general with rspec + # matchers you expect to see something like: + # + # expect(foo).to receive(:bar).with( + # 'one', + # 'two', + # 'three', + # 'four', + # 'five' + # ) + # + # In this case the arguments are aligned to the left side as opposed to + # being aligned with the `receive` call. + if %w[to not_to to_not].include?(message.value) + 0 + else + width = doc_width(doc) + 1 + width > (q.maxwidth / 2) ? 0 : width + end + end end # :call-seq: @@ -4422,7 +4459,7 @@ def format(q) if target q.format(target) - q.format(CallOperatorFormatter.new(operator)) + q.format(CallOperatorFormatter.new(operator), stackable: false) end q.format(name) @@ -4657,7 +4694,7 @@ def format(q) q.group do q.text("def ") q.format(target) - q.format(CallOperatorFormatter.new(operator)) + q.format(CallOperatorFormatter.new(operator), stackable: false) q.format(name) q.format(params) if !params.is_a?(Params) || !params.empty? end @@ -5875,7 +5912,7 @@ def child_nodes def format(q) q.group do q.format(parent) - q.format(CallOperatorFormatter.new(operator)) + q.format(CallOperatorFormatter.new(operator), stackable: false) q.format(name) end end @@ -6292,23 +6329,15 @@ def child_nodes end def format(q) - contents = -> do - q.format(lbrace) - - if assocs.empty? - q.breakable("") - else - q.indent do - q.breakable - q.format(HashFormatter.for(self)) - end - q.breakable - end - - q.text("}") + if q.parent.is_a?(Assoc) + format_contents(q) + else + q.group { format_contents(q) } end + end - q.parent.is_a?(Assoc) ? contents.call : q.group(&contents) + def format_key(q, key) + (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) end def pretty_print(q) @@ -6329,6 +6358,24 @@ def to_json(*opts) *opts ) end + + private + + def format_contents(q) + q.format(lbrace) + + if assocs.empty? + q.breakable("") + else + q.indent do + q.breakable + q.seplist(assocs) { |assoc| q.format(assoc) } + end + q.breakable + end + + q.text("}") + end end # :call-seq: @@ -6551,12 +6598,6 @@ def initialize(key, value) @value = value end - # This is here so that when checking if its contained within a parent - # pattern that it will return true. - def class - HshPtn - end - def comments [] end @@ -6620,7 +6661,9 @@ def child_nodes 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) } } + contents = -> do + q.seplist(parts) { |part| q.format(part, stackable: false) } + end if constant q.format(constant) @@ -6766,6 +6809,23 @@ def on_ident(value) ) end + # If the predicate of a conditional or loop contains an assignment (in which + # case we can't know for certain that that assignment doesn't impact the + # statements inside the conditional) then we can't use the modifier form + # and we must use the block form. + module ContainsAssignment + def self.call(parent) + queue = [parent] + + while node = queue.shift + return true if [Assign, MAssign, OpAssign].include?(node.class) + queue += node.child_nodes + end + + false + end + end + # Formats an If or Unless node. class ConditionalFormatter # [String] the keyword associated with this conditional @@ -6784,7 +6844,7 @@ def format(q) # case we can't know for certain that that assignment doesn't impact the # statements inside the conditional) then we can't use the modifier form # and we must use the block form. - if [Assign, MAssign, OpAssign].include?(node.predicate.class) + if ContainsAssignment.call(node.predicate) format_break(q, force: true) return end @@ -7060,23 +7120,31 @@ def initialize(keyword, 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 - Parentheses.flat(q) do - q.format(node.statement) - q.text(" #{keyword} ") - q.format(node.predicate) - end - end + if ContainsAssignment.call(node.statement) + q.group { format_flat(q) } + else + q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } + end + end + + private + + def format_break(q) + 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 + + def format_flat(q) + Parentheses.flat(q) do + q.format(node.statement) + q.text(" #{keyword} ") + q.format(node.predicate) end end end @@ -7314,7 +7382,12 @@ def on_in(pattern, statements, consequent) beginning = find_token(Kw, "in") ending = consequent || find_token(Kw, "end") - statements_start = find_token(Kw, "then", consume: false) || pattern + statements_start = pattern + if token = find_token(Kw, "then", consume: false) + tokens.delete(token) + statements_start = token + end + statements.bind( find_next_statement_start(statements_start.location.end_char), ending.location.start_char @@ -7857,6 +7930,10 @@ def initialize(value:, location:, comments: []) @comments = comments end + def child_nodes + [] + end + def format(q) q.text(value) end @@ -9077,16 +9154,15 @@ 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? @@ -11204,8 +11280,10 @@ def on_string_embexpr(statements) Location.new( start_line: embexpr_beg.location.start_line, start_char: embexpr_beg.location.start_char, - end_line: - [embexpr_end.location.end_line, statements.location.end_line].max, + end_line: [ + embexpr_end.location.end_line, + statements.location.end_line + ].max, end_char: embexpr_end.location.end_char ) @@ -11315,8 +11393,10 @@ def on_string_literal(string) Location.new( start_line: tstring_beg.location.start_line, start_char: tstring_beg.location.start_char, - end_line: - [tstring_end.location.end_line, string.location.end_line].max, + end_line: [ + tstring_end.location.end_line, + string.location.end_line + ].max, end_char: tstring_end.location.end_char ) @@ -12418,11 +12498,7 @@ def initialize(keyword, node, statements) end def format(q) - # If the predicate of the loop contains an assignment (in which case we - # can't know for certain that that assignment doesn't impact the - # statements inside the loop) then we can't use the modifier form and we - # must use the block form. - if [Assign, MAssign, OpAssign].include?(node.predicate.class) + if ContainsAssignment.call(node.predicate) format_break(q) q.break_parent return @@ -12583,8 +12659,13 @@ def format(q) # foo # end until bar # - # The above is effectively a `do...until` loop. - if statement.is_a?(Begin) + # Also, if the statement of the modifier includes an assignment, then we + # can't know for certain that it won't impact the predicate, so we need to + # force it to stay as it is. This looks like: + # + # foo = bar until foo + # + if statement.is_a?(Begin) || ContainsAssignment.call(statement) q.format(statement) q.text(" until ") q.format(predicate) @@ -13027,7 +13108,12 @@ def on_when(arguments, statements, consequent) beginning = find_token(Kw, "when") ending = consequent || find_token(Kw, "end") - statements_start = find_token(Kw, "then", consume: false) || arguments + statements_start = arguments + if token = find_token(Kw, "then", consume: false) + tokens.delete(token) + statements_start = token + end + statements.bind( find_next_statement_start(statements_start.location.end_char), ending.location.start_char @@ -13171,8 +13257,13 @@ def format(q) # foo # end while bar # - # The above is effectively a `do...while` loop. - if statement.is_a?(Begin) + # Also, if the statement of the modifier includes an assignment, then we + # can't know for certain that it won't impact the predicate, so we need to + # force it to stay as it is. This looks like: + # + # foo = bar while foo + # + if statement.is_a?(Begin) || ContainsAssignment.call(statement) q.format(statement) q.text(" while ") q.format(predicate) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index eb68214d..82545df7 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -55,7 +55,7 @@ class UnformattedError < StandardError def run(filepath, source) raise UnformattedError if source != SyntaxTree.format(source) - rescue + rescue StandardError warn("[#{Color.yellow("warn")}] #{filepath}") raise end @@ -82,7 +82,7 @@ def run(filepath, source) if formatted != SyntaxTree.format(formatted) raise NonIdempotentFormatError end - rescue + rescue StandardError warn(warning) raise end @@ -126,7 +126,7 @@ def run(filepath, source) delta = ((Time.now - start) * 1000).round puts "\r#{color} #{delta}ms" - rescue + rescue StandardError puts "\r#{filepath}" raise end @@ -174,7 +174,7 @@ def run(argv) patterns.each do |pattern| Dir.glob(pattern).each do |filepath| next unless File.file?(filepath) - source = source_for(filepath) + source = SyntaxTree.read(filepath) begin action.run(filepath, source) @@ -210,19 +210,6 @@ def run(argv) private - # Returns the source from the given filepath taking into account any - # potential magic encoding comments. - def source_for(filepath) - encoding = - File.open(filepath, "r") do |file| - header = file.readline - header += file.readline if header.start_with?("#!") - Ripper.new(header).tap(&:parse).encoding - end - - File.read(filepath, encoding: encoding) - end - # Highlights a snippet from a source and parse error. def highlight_error(error, source) lines = source.lines diff --git a/lib/syntax_tree/prettyprint.rb b/lib/syntax_tree/prettyprint.rb index 48aeff56..8ca5d555 100644 --- a/lib/syntax_tree/prettyprint.rb +++ b/lib/syntax_tree/prettyprint.rb @@ -113,8 +113,10 @@ def indent? def pretty_print(q) q.text("breakable") - attributes = - [("force=true" if force?), ("indent=false" unless indent?)].compact + attributes = [ + ("force=true" if force?), + ("indent=false" unless indent?) + ].compact if attributes.any? q.text("(") diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index a0428314..b8a9c2fb 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -3,5 +3,5 @@ require "ripper" class SyntaxTree < Ripper - VERSION = "1.1.0" + VERSION = "1.1.1" end diff --git a/test/behavior_test.rb b/test/behavior_test.rb new file mode 100644 index 00000000..81308464 --- /dev/null +++ b/test/behavior_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class SyntaxTree + class BehaviorTest < Minitest::Test + def test_empty + void_stmt = SyntaxTree.parse("").statements.body.first + assert_kind_of(VoidStmt, void_stmt) + end + + 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 + end +end diff --git a/test/fixtures/assoc.rb b/test/fixtures/assoc.rb index 293f4e26..ceed0d0c 100644 --- a/test/fixtures/assoc.rb +++ b/test/fixtures/assoc.rb @@ -14,3 +14,27 @@ } - { foo: bar } +% +{ + foo: [ + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + ] +} +% +{ + foo: { + fooooooooooooooooooooooooooooooooo: ooooooooooooooooooooooooooooooooooooooo + } +} +% +{ + foo: -> do + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + end +} +% +{ + foo: %w[ + foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + ] +} diff --git a/test/fixtures/command_call.rb b/test/fixtures/command_call.rb index 9ad0a3ac..955c4bfc 100644 --- a/test/fixtures/command_call.rb +++ b/test/fixtures/command_call.rb @@ -5,3 +5,21 @@ - foo.bar barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +% +expect(foo).to receive(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +expect(foo).to receive( + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) +% +expect(foo).not_to receive(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +expect(foo).not_to receive( + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) +% +expect(foo).to_not receive(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) +- +expect(foo).to_not receive( + fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +) diff --git a/test/fixtures/if.rb b/test/fixtures/if.rb index 38c67e52..4331abb1 100644 --- a/test/fixtures/if.rb +++ b/test/fixtures/if.rb @@ -24,3 +24,7 @@ if foo += 1 foo end +% +if (foo += 1) + foo +end diff --git a/test/fixtures/if_mod.rb b/test/fixtures/if_mod.rb index 04e529a8..ea80e623 100644 --- a/test/fixtures/if_mod.rb +++ b/test/fixtures/if_mod.rb @@ -8,3 +8,8 @@ end % bar if foo # comment +% +foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr if foo +- +foo = + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr if foo diff --git a/test/fixtures/unless.rb b/test/fixtures/unless.rb index f5b01c3f..95a40c38 100644 --- a/test/fixtures/unless.rb +++ b/test/fixtures/unless.rb @@ -24,3 +24,7 @@ unless foo += 1 foo end +% +unless (foo += 1) + foo +end diff --git a/test/fixtures/unless_mod.rb b/test/fixtures/unless_mod.rb index 30b73f37..e2dbb764 100644 --- a/test/fixtures/unless_mod.rb +++ b/test/fixtures/unless_mod.rb @@ -8,3 +8,8 @@ end % bar unless foo # comment +% +foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr unless foo +- +foo = + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr unless foo diff --git a/test/fixtures/until.rb b/test/fixtures/until.rb index 4eb2c8cb..778e3fb0 100644 --- a/test/fixtures/until.rb +++ b/test/fixtures/until.rb @@ -19,3 +19,7 @@ until foo += 1 foo end +% +until (foo += 1) + foo +end diff --git a/test/fixtures/until_mod.rb b/test/fixtures/until_mod.rb index f3bc7ce3..869cc1e7 100644 --- a/test/fixtures/until_mod.rb +++ b/test/fixtures/until_mod.rb @@ -8,3 +8,8 @@ end % bar until foo # comment +% +foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr until foo +- +foo = + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr until foo diff --git a/test/fixtures/while.rb b/test/fixtures/while.rb index 3dcf839d..1404f07d 100644 --- a/test/fixtures/while.rb +++ b/test/fixtures/while.rb @@ -19,3 +19,7 @@ while foo += 1 foo end +% +while (foo += 1) + foo +end diff --git a/test/fixtures/while_mod.rb b/test/fixtures/while_mod.rb index 482f3934..18a07b4c 100644 --- a/test/fixtures/while_mod.rb +++ b/test/fixtures/while_mod.rb @@ -8,3 +8,8 @@ end % bar while foo # comment +% +foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr while foo +- +foo = + barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr while foo diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb new file mode 100644 index 00000000..ba2bc1c0 --- /dev/null +++ b/test/idempotency_test.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +return if ENV["FAST"] +require_relative "test_helper" + +class SyntaxTree + class IdempotencyTest < Minitest::Test + Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath| + define_method(:"test_#{filepath}") do + source = SyntaxTree.read(filepath) + formatted = SyntaxTree.format(source) + + assert_equal(formatted, SyntaxTree.format(formatted), "expected #{filepath} to be formatted idempotently") + end + end + end +end diff --git a/test/syntax_tree_test.rb b/test/node_test.rb similarity index 94% rename from test/syntax_tree_test.rb rename to test/node_test.rb index 3893b25c..84dc4891 100644 --- a/test/syntax_tree_test.rb +++ b/test/node_test.rb @@ -1,54 +1,9 @@ # 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" +require_relative "test_helper" class SyntaxTree - class SyntaxTreeTest < Minitest::Test - # -------------------------------------------------------------------------- - # Tests for behavior - # -------------------------------------------------------------------------- - - def test_empty - void_stmt = SyntaxTree.parse("").statements.body.first - assert_kind_of(VoidStmt, void_stmt) - end - - 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 - # -------------------------------------------------------------------------- - + class NodeTest < Minitest::Test def test_BEGIN assert_node(BEGINBlock, "BEGIN", "BEGIN {}") end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..8512253c --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,11 @@ +# 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"