diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c40e40..018d5b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [6.0.1] - 2023-02-26 + +### Added + +- The class declarations returned as the result of the indexing operation now have their superclass as a field. It is returned as an array of constants. If the superclass is anything other than a constant lookup, then it raises an error. + +### Changed + +- The `nesting` field on the results of the indexing operation is no longer a single flat array. Instead it is an array of arrays, where each array is a single nesting level. This more accurately reflects the nesting of the nodes in the tree. For example, `class Foo::Bar::Baz; end` would result in `[Foo, Bar, Baz]`, but that incorrectly implies that you can see constants at each of those levels. Now this would result in `[[Foo, Bar, Baz]]` to indicate that it can see either the top level or constants within the scope of `Foo::Bar::Baz` only. +- When formatting hashes that have omitted values and mixed hash rockets with labels, the formatting now maintains whichever delimiter was used in the source. This is because forcing the use of hash rockets with omitted values results in a syntax error. +- Handle the case where a bare hash is used after the `break`, `next`, or `return` keywords. Previously this would result in hash labels which is not valid syntax. Now it maintains the delimiters used in the source. +- The `<<` operator will now break on chained `<<` expressions. Previously it would always stay flat. + ## [6.0.0] - 2023-02-10 ### Added @@ -559,7 +572,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.1...HEAD +[6.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.0...v6.0.1 [6.0.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.3.0...v6.0.0 [5.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.2.0...v5.3.0 [5.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.1.0...v5.2.0 diff --git a/Gemfile.lock b/Gemfile.lock index 325d89b3..c7ffc7d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (6.0.0) + syntax_tree (6.0.1) prettier_print (>= 1.2.0) GEM @@ -12,25 +12,25 @@ GEM json (2.6.3) minitest (5.17.0) parallel (1.22.1) - parser (3.2.0.0) + parser (3.2.1.0) ast (~> 2.4.1) prettier_print (1.2.0) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.6.2) + regexp_parser (2.7.0) rexml (3.2.5) - rubocop (1.45.1) + rubocop (1.46.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.26.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) + rubocop-ast (1.26.0) + parser (>= 3.2.1.0) ruby-progressbar (1.11.0) simplecov (0.22.0) docile (~> 1.1) diff --git a/README.md b/README.md index 500d5fad..03942d46 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ It is built with only standard library dependencies. It additionally ships with - [SyntaxTree.format(source)](#syntaxtreeformatsource) - [SyntaxTree.mutation(&block)](#syntaxtreemutationblock) - [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block) + - [SyntaxTree.index(source)](#syntaxtreeindexsource) - [Nodes](#nodes) - [child_nodes](#child_nodes) - [copy(**attrs)](#copyattrs) @@ -347,6 +348,10 @@ This function yields a new mutation visitor to the block, and then returns the i This function takes an input string containing Ruby code, an input string containing a valid Ruby `in` clause expression that can be used to match against nodes in the tree (can be generated using `stree expr`, `stree match`, or `Node#construct_keys`), and a block. Each node that matches the given query will be yielded to the block. The block will receive the node as its only argument. +### SyntaxTree.index(source) + +This function takes an input string containing Ruby code and returns a list of all of the class declarations, module declarations, and method definitions within a file. Each of the entries also has access to its associated comments. This is useful for generating documentation or index information for a file to support something like go-to-definition. + ## Nodes There are many different node types in the syntax tree. They are meant to be treated as immutable structs containing links to child nodes with minimal logic contained within their implementation. However, for the most part they all respond to a certain set of APIs, listed below. diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index ab2460dd..c6973847 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -20,11 +20,12 @@ def initialize(line, column) # This entry represents a class definition using the class keyword. class ClassDefinition - attr_reader :nesting, :name, :location, :comments + attr_reader :nesting, :name, :superclass, :location, :comments - def initialize(nesting, name, location, comments) + def initialize(nesting, name, superclass, location, comments) @nesting = nesting @name = name + @superclass = superclass @location = location @comments = comments end @@ -176,30 +177,101 @@ def location_for(iseq) Location.new(code_location[0], code_location[1]) end + def find_constant_path(insns, index) + index -= 1 while insns[index].is_a?(Integer) + insn = insns[index] + + if insn.is_a?(Array) && insn[0] == :opt_getconstant_path + # In this case we're on Ruby 3.2+ and we have an opt_getconstant_path + # instruction, so we already know all of the symbols in the nesting. + [index - 1, insn[1]] + elsif insn.is_a?(Symbol) && insn.match?(/\Alabel_\d+/) + # Otherwise, if we have a label then this is very likely the + # destination of an opt_getinlinecache instruction, in which case + # we'll walk backwards to grab up all of the constants. + names = [] + + index -= 1 + until insns[index].is_a?(Array) && + insns[index][0] == :opt_getinlinecache + if insns[index].is_a?(Array) && insns[index][0] == :getconstant + names.unshift(insns[index][1]) + end + + index -= 1 + end + + [index - 1, names] + else + [index, []] + end + end + def index_iseq(iseq, file_comments) results = [] queue = [[iseq, []]] while (current_iseq, current_nesting = queue.shift) - current_iseq[13].each_with_index do |insn, index| - next unless insn.is_a?(Array) + line = current_iseq[8] + insns = current_iseq[13] + + insns.each_with_index do |insn, index| + case insn + when Integer + line = insn + next + when Array + # continue on + else + # skip everything else + next + end case insn[0] when :defineclass _, name, class_iseq, flags = insn + next_nesting = current_nesting.dup + + # This is the index we're going to search for the nested constant + # path within the declaration name. + constant_index = index - 2 + + # This is the superclass of the class being defined. + superclass = [] + + # If there is a superclass, then we're going to find it here and + # then update the constant_index as necessary. + if flags & VM_DEFINECLASS_FLAG_HAS_SUPERCLASS > 0 + constant_index, superclass = + find_constant_path(insns, index - 1) + + if superclass.empty? + raise NotImplementedError, + "superclass with non constant path on line #{line}" + end + end + + if (_, nesting = find_constant_path(insns, constant_index)) + # If there is a constant path in the class name, then we need to + # handle that by updating the nesting. + next_nesting << (nesting << name) + else + # Otherwise we'll add the class name to the nesting. + next_nesting << [name] + end if flags == VM_DEFINECLASS_TYPE_SINGLETON_CLASS # At the moment, we don't support singletons that aren't # defined on self. We could, but it would require more # emulation. - if current_iseq[13][index - 2] != [:putself] + if insns[index - 2] != [:putself] raise NotImplementedError, "singleton class with non-self receiver" end elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0 location = location_for(class_iseq) results << ModuleDefinition.new( - current_nesting, + next_nesting, name, location, EntryComments.new(file_comments, location) @@ -207,14 +279,15 @@ def index_iseq(iseq, file_comments) else location = location_for(class_iseq) results << ClassDefinition.new( - current_nesting, + next_nesting, name, + superclass, location, EntryComments.new(file_comments, location) ) end - queue << [class_iseq, current_nesting + [name]] + queue << [class_iseq, next_nesting] when :definemethod location = location_for(insn[2]) results << MethodDefinition.new( @@ -259,24 +332,43 @@ def initialize visit_methods do def visit_class(node) - name = visit(node.constant).to_sym + names = visit(node.constant) + nesting << names + location = Location.new(node.location.start_line, node.location.start_column) + superclass = + if node.superclass + visited = visit(node.superclass) + + if visited == [[]] + raise NotImplementedError, "superclass with non constant path" + end + + visited + else + [] + end + results << ClassDefinition.new( nesting.dup, - name, + names.last, + superclass, location, comments_for(node) ) - nesting << name super nesting.pop end def visit_const_ref(node) - node.constant.value + [node.constant.value.to_sym] + end + + def visit_const_path_ref(node) + visit(node.parent) << node.constant.value.to_sym end def visit_def(node) @@ -302,18 +394,19 @@ def visit_def(node) end def visit_module(node) - name = visit(node.constant).to_sym + names = visit(node.constant) + nesting << names + location = Location.new(node.location.start_line, node.location.start_column) results << ModuleDefinition.new( nesting.dup, - name, + names.last, location, comments_for(node) ) - nesting << name super nesting.pop end @@ -327,6 +420,10 @@ def visit_statements(node) @statements = node super end + + def visit_var_ref(node) + [node.value.value.to_sym] + end end private diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 567ec0c8..c4bc1495 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -1780,13 +1780,25 @@ def format_key(q, key) end def self.for(container) - labels = - container.assocs.all? do |assoc| - next true if assoc.is_a?(AssocSplat) - + container.assocs.each do |assoc| + if assoc.is_a?(AssocSplat) + # Splat nodes do not impact the formatting choice. + elsif assoc.value.nil? + # If the value is nil, then it has been omitted. In this case we have + # to match the existing formatting because standardizing would + # potentially break the code. For example: + # + # { first:, "second" => "value" } + # + return Identity.new + else + # Otherwise, we need to check the type of the key. If it's a label or + # dynamic symbol, we can use labels. If it's a symbol literal then it + # needs to match a certain pattern to be used as a label. If it's + # anything else, then we need to use hash rockets. case assoc.key - when Label - true + when Label, DynaSymbol + # Here labels can be used. when SymbolLiteral # When attempting to convert a hash rocket into a hash label, # you need to take care because only certain patterns are @@ -1794,15 +1806,18 @@ def self.for(container) # 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 + + if !value.match?(/^[_A-Za-z]/) || value.end_with?("=") + return Rockets.new + end else - false + # If the value is anything else, we have to use hash rockets. + return Rockets.new end end + end - (labels ? Labels : Rockets).new + Labels.new end end @@ -1859,7 +1874,15 @@ def ===(other) end def format_key(q, key) - (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) + @key_formatter ||= + case q.parents.take(3).last + when Break, Next, ReturnNode + HashKeyFormatter::Identity.new + else + HashKeyFormatter.for(self) + end + + @key_formatter.format_key(q, key) end end @@ -2074,10 +2097,15 @@ def format(q) q.group { q.format(left) } q.text(" ") unless power - if operator == :<< - q.text("<< ") - q.format(right) - else + if operator != :<< + q.group do + q.text(operator.name) + q.indent do + power ? q.breakable_empty : q.breakable_space + q.format(right) + end + end + elsif left.is_a?(Binary) && left.operator == :<< q.group do q.text(operator.name) q.indent do @@ -2085,6 +2113,9 @@ def format(q) q.format(right) end end + else + q.text("<< ") + q.format(right) end end end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 426bd945..d0a5bf67 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -1559,7 +1559,14 @@ def on_elsif(predicate, statements, consequent) beginning = consume_keyword(:elsif) ending = consequent || consume_keyword(:end) - start_char = find_next_statement_start(predicate.location.end_char) + delimiter = + find_keyword_between(:then, predicate, statements) || + find_token_between(Semicolon, predicate, statements) + + tokens.delete(delimiter) if delimiter + start_char = + find_next_statement_start((delimiter || predicate).location.end_char) + statements.bind( self, start_char, @@ -2045,6 +2052,7 @@ def on_if(predicate, statements, consequent) start_char = find_next_statement_start((keyword || predicate).location.end_char) + statements.bind( self, start_char, @@ -3805,6 +3813,7 @@ def on_unless(predicate, statements, consequent) start_char = find_next_statement_start((keyword || predicate).location.end_char) + statements.bind( self, start_char, diff --git a/lib/syntax_tree/translation/parser.rb b/lib/syntax_tree/translation/parser.rb index ad889478..8be4fc79 100644 --- a/lib/syntax_tree/translation/parser.rb +++ b/lib/syntax_tree/translation/parser.rb @@ -336,8 +336,8 @@ def visit_assign(node) # Visit an Assoc node. def visit_assoc(node) if node.value.nil? + # { foo: } expression = srange(node.start_char, node.end_char - 1) - type, location = if node.key.value.start_with?(/[A-Z]/) [:const, smap_constant(nil, expression, expression)] @@ -356,13 +356,38 @@ def visit_assoc(node) srange_node(node) ) ) - else + elsif node.key.is_a?(Label) + # { foo: 1 } s( :pair, [visit(node.key), visit(node.value)], smap_operator( - srange_search_between(node.key, node.value, "=>") || - srange_length(node.key.end_char, -1), + srange_length(node.key.end_char, -1), + srange_node(node) + ) + ) + elsif (operator = srange_search_between(node.key, node.value, "=>")) + # { :foo => 1 } + s( + :pair, + [visit(node.key), visit(node.value)], + smap_operator(operator, srange_node(node)) + ) + else + # { "foo": 1 } + key = visit(node.key) + key_location = + smap_collection( + key.location.begin, + srange_length(node.key.end_char - 2, 1), + srange(node.key.start_char, node.key.end_char - 1) + ) + + s( + :pair, + [s(key.type, key.children, key_location), visit(node.value)], + smap_operator( + srange_length(node.key.end_char, -1), srange_node(node) ) ) @@ -769,7 +794,11 @@ def visit_command_call(node) srange(node.start_char, end_char) elsif node.block - srange_node(node.message) + if node.receiver + srange(node.receiver.start_char, node.message.end_char) + else + srange_node(node.message) + end else srange_node(node) end @@ -1010,6 +1039,21 @@ def visit_else(node) # Visit an Elsif node. def visit_elsif(node) + begin_start = node.predicate.end_char + begin_end = + if node.statements.empty? + node.statements.end_char + else + node.statements.body.first.start_char + end + + begin_token = + if buffer.source[begin_start...begin_end].include?("then") + srange_find(begin_start, begin_end, "then") + elsif buffer.source[begin_start...begin_end].include?(";") + srange_find(begin_start, begin_end, ";") + end + else_token = case node.consequent when Elsif @@ -1029,7 +1073,7 @@ def visit_elsif(node) ], smap_condition( srange_length(node.start_char, 5), - nil, + begin_token, else_token, nil, expression @@ -1287,35 +1331,13 @@ def visit_ident(node) # Visit an IfNode node. def visit_if(node) - predicate = - case node.predicate - when RangeNode - type = - node.predicate.operator.value == ".." ? :iflipflop : :eflipflop - s(type, visit(node.predicate).children, nil) - when RegexpLiteral - s(:match_current_line, [visit(node.predicate)], nil) - when Unary - if node.predicate.operator.value == "!" && - node.predicate.statement.is_a?(RegexpLiteral) - s( - :send, - [ - s(:match_current_line, [visit(node.predicate.statement)]), - :! - ], - nil - ) - else - visit(node.predicate) - end - else - visit(node.predicate) - end - s( :if, - [predicate, visit(node.statements), visit(node.consequent)], + [ + visit_predicate(node.predicate), + visit(node.statements), + visit(node.consequent) + ], if node.modifier? smap_keyword_bare( srange_find_between(node.statements, node.predicate, "if"), @@ -1551,12 +1573,14 @@ def visit_lambda_var(node) location = if node.start_char == node.end_char smap_collection_bare(nil) - else + elsif buffer.source[node.start_char - 1] == "(" smap_collection( srange_length(node.start_char, 1), srange_length(node.end_char, -1), srange_node(node) ) + else + smap_collection_bare(srange_node(node)) end s(:args, visit(node.params).children + shadowargs, location) @@ -1577,28 +1601,20 @@ def visit_massign(node) # Visit a MethodAddBlock node. def visit_method_add_block(node) case node.call - when Break, Next, ReturnNode - type, arguments = block_children(node.block) - call = visit(node.call) - - s( - call.type, - [ - s( - type, - [*call.children, arguments, visit(node.block.bodystmt)], - nil - ) - ], - nil - ) when ARef, Super, ZSuper type, arguments = block_children(node.block) s( type, [visit(node.call), arguments, visit(node.block.bodystmt)], - nil + smap_collection( + srange_node(node.block.opening), + srange_length( + node.block.end_char, + node.block.keywords? ? -3 : -1 + ), + srange_node(node) + ) ) else visit_command_call( @@ -2274,7 +2290,16 @@ def visit_super(node) ) ) when ArgsForward - s(:super, [visit(node.arguments.arguments)], nil) + s( + :super, + [visit(node.arguments.arguments)], + smap_keyword( + srange_length(node.start_char, 5), + srange_find(node.start_char + 5, node.end_char, "("), + srange_length(node.end_char, -1), + srange_node(node) + ) + ) else s( :super, @@ -2376,22 +2401,58 @@ def visit_tstring_content(node) # Visit a Unary node. def visit_unary(node) # Special handling here for flipflops - if node.statement.is_a?(Paren) && - node.statement.contents.is_a?(Statements) && - node.statement.contents.body.length == 1 && - (range = node.statement.contents.body.first).is_a?(RangeNode) && + if (paren = node.statement).is_a?(Paren) && + paren.contents.is_a?(Statements) && + paren.contents.body.length == 1 && + (range = paren.contents.body.first).is_a?(RangeNode) && node.operator == "!" - type = range.operator.value == ".." ? :iflipflop : :eflipflop - return( - s( - :send, - [s(:begin, [s(type, visit(range).children, nil)], nil), :!], - nil + s( + :send, + [ + s( + :begin, + [ + s( + range.operator.value == ".." ? :iflipflop : :eflipflop, + visit(range).children, + smap_operator( + srange_node(range.operator), + srange_node(range) + ) + ) + ], + smap_collection( + srange_length(paren.start_char, 1), + srange_length(paren.end_char, -1), + srange_node(paren) + ) + ), + :! + ], + smap_send_bare( + srange_length(node.start_char, 1), + srange_node(node) + ) + ) + elsif node.operator == "!" && node.statement.is_a?(RegexpLiteral) + s( + :send, + [ + s( + :match_current_line, + [visit(node.statement)], + smap(srange_node(node.statement)) + ), + :! + ], + smap_send_bare( + srange_length(node.start_char, 1), + srange_node(node) ) ) + else + visit(canonical_unary(node)) end - - visit(canonical_unary(node)) end # Visit an Undef node. @@ -2408,41 +2469,43 @@ def visit_undef(node) # Visit an UnlessNode node. def visit_unless(node) - predicate = - case node.predicate - when RegexpLiteral - s(:match_current_line, [visit(node.predicate)], nil) - when Unary - if node.predicate.operator.value == "!" && - node.predicate.statement.is_a?(RegexpLiteral) - s( - :send, - [ - s(:match_current_line, [visit(node.predicate.statement)]), - :! - ], - nil - ) - else - visit(node.predicate) - end - else - visit(node.predicate) - end - s( :if, - [predicate, visit(node.consequent), visit(node.statements)], + [ + visit_predicate(node.predicate), + visit(node.consequent), + visit(node.statements) + ], if node.modifier? smap_keyword_bare( srange_find_between(node.statements, node.predicate, "unless"), srange_node(node) ) else + begin_start = node.predicate.end_char + begin_end = + if node.statements.empty? + node.statements.end_char + else + node.statements.body.first.start_char + end + + begin_token = + if buffer.source[begin_start...begin_end].include?("then") + srange_find(begin_start, begin_end, "then") + elsif buffer.source[begin_start...begin_end].include?(";") + srange_find(begin_start, begin_end, ";") + end + + else_token = + if node.consequent + srange_length(node.consequent.start_char, 4) + end + smap_condition( srange_length(node.start_char, 6), - srange_search_between(node.predicate, node.statements, "then"), - nil, + begin_token, + else_token, srange_length(node.end_char, -3), srange_node(node) ) @@ -3014,6 +3077,31 @@ def srange_node(node) location = node.location srange(location.start_char, location.end_char) end + + def visit_predicate(node) + case node + when RangeNode + s( + node.operator.value == ".." ? :iflipflop : :eflipflop, + visit(node).children, + smap_operator(srange_node(node.operator), srange_node(node)) + ) + when RegexpLiteral + s(:match_current_line, [visit(node)], smap(srange_node(node))) + when Unary + if node.operator.value == "!" && node.statement.is_a?(RegexpLiteral) + s( + :send, + [s(:match_current_line, [visit(node.statement)]), :!], + smap_send_bare(srange_node(node.operator), srange_node(node)) + ) + else + visit(node) + end + else + visit(node) + end + end end end end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index 1f028f89..0b3502d1 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "6.0.0" + VERSION = "6.0.1" end diff --git a/test/fixtures/binary.rb b/test/fixtures/binary.rb index f8833cdc..4cb56cbf 100644 --- a/test/fixtures/binary.rb +++ b/test/fixtures/binary.rb @@ -3,6 +3,11 @@ % foo << bar % +foo << barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr << barrrrrrrrrrrrr << barrrrrrrrrrrrrrrrrr +- +foo << barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr << barrrrrrrrrrrrr << + barrrrrrrrrrrrrrrrrr +% foo**bar % foo * barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr diff --git a/test/fixtures/break.rb b/test/fixtures/break.rb index a608a6b2..519becda 100644 --- a/test/fixtures/break.rb +++ b/test/fixtures/break.rb @@ -33,3 +33,5 @@ qux end ) +% +break :foo => "bar" diff --git a/test/fixtures/hash.rb b/test/fixtures/hash.rb index 9c43a4fe..70e89f69 100644 --- a/test/fixtures/hash.rb +++ b/test/fixtures/hash.rb @@ -29,3 +29,5 @@ { # comment } +% # >= 3.1.0 +{ foo:, "bar" => "baz" } diff --git a/test/fixtures/next.rb b/test/fixtures/next.rb index 79a8c62e..66e90028 100644 --- a/test/fixtures/next.rb +++ b/test/fixtures/next.rb @@ -72,3 +72,5 @@ fun foo do end ) +% +next :foo => "bar" diff --git a/test/fixtures/return.rb b/test/fixtures/return.rb index 8f7d0aa3..7092464f 100644 --- a/test/fixtures/return.rb +++ b/test/fixtures/return.rb @@ -37,3 +37,5 @@ return [] % return [1] +% +return :foo => "bar" diff --git a/test/index_test.rb b/test/index_test.rb index 6bb83881..60c51d9d 100644 --- a/test/index_test.rb +++ b/test/index_test.rb @@ -7,14 +7,14 @@ class IndexTest < Minitest::Test def test_module index_each("module Foo; end") do |entry| assert_equal :Foo, entry.name - assert_empty entry.nesting + assert_equal [[:Foo]], entry.nesting end end def test_module_nested index_each("module Foo; module Bar; end; end") do |entry| assert_equal :Bar, entry.name - assert_equal [:Foo], entry.nesting + assert_equal [[:Foo], [:Bar]], entry.nesting end end @@ -28,14 +28,65 @@ def test_module_comments def test_class index_each("class Foo; end") do |entry| assert_equal :Foo, entry.name - assert_empty entry.nesting + assert_equal [[:Foo]], entry.nesting + end + end + + def test_class_paths_2 + index_each("class Foo::Bar; end") do |entry| + assert_equal :Bar, entry.name + assert_equal [%i[Foo Bar]], entry.nesting + end + end + + def test_class_paths_3 + index_each("class Foo::Bar::Baz; end") do |entry| + assert_equal :Baz, entry.name + assert_equal [%i[Foo Bar Baz]], entry.nesting end end def test_class_nested index_each("class Foo; class Bar; end; end") do |entry| assert_equal :Bar, entry.name - assert_equal [:Foo], entry.nesting + assert_equal [[:Foo], [:Bar]], entry.nesting + end + end + + def test_class_paths_nested + index_each("class Foo; class Bar::Baz::Qux; end; end") do |entry| + assert_equal :Qux, entry.name + assert_equal [[:Foo], %i[Bar Baz Qux]], entry.nesting + end + end + + def test_class_superclass + index_each("class Foo < Bar; end") do |entry| + assert_equal :Foo, entry.name + assert_equal [[:Foo]], entry.nesting + assert_equal [:Bar], entry.superclass + end + end + + def test_class_path_superclass + index_each("class Foo::Bar < Baz::Qux; end") do |entry| + assert_equal :Bar, entry.name + assert_equal [%i[Foo Bar]], entry.nesting + assert_equal %i[Baz Qux], entry.superclass + end + end + + def test_class_path_superclass_unknown + source = "class Foo < bar; end" + + assert_raises NotImplementedError do + Index.index(source, backend: Index::ParserBackend.new) + end + + if defined?(RubyVM::InstructionSequence) + assert_raises NotImplementedError do + Index.index(source, backend: Index::ISeqBackend.new) + end end end @@ -56,7 +107,7 @@ def test_method def test_method_nested index_each("class Foo; def foo; end; end") do |entry| assert_equal :foo, entry.name - assert_equal [:Foo], entry.nesting + assert_equal [[:Foo]], entry.nesting end end @@ -77,7 +128,7 @@ def test_singleton_method def test_singleton_method_nested index_each("class Foo; def self.foo; end; end") do |entry| assert_equal :foo, entry.name - assert_equal [:Foo], entry.nesting + assert_equal [[:Foo]], entry.nesting end end diff --git a/test/translation/parser_test.rb b/test/translation/parser_test.rb index ad87d8c6..1df98f47 100644 --- a/test/translation/parser_test.rb +++ b/test/translation/parser_test.rb @@ -55,7 +55,6 @@ class ParserTest < Minitest::Test "test_dedenting_heredoc:399", "test_slash_newline_in_heredocs:7194", "test_parser_slash_slash_n_escaping_in_literals:*", - "test_cond_match_current_line:4801", "test_forwarded_restarg:*", "test_forwarded_kwrestarg:*", "test_forwarded_argument_with_restarg:*",