diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index e54c9100..8ca265e0 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.3.6 + uses: dependabot/fetch-metadata@v1.6.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 5b662631..4bbfc0a2 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v3 - name: Set up Ruby @@ -39,7 +39,7 @@ jobs: rdoc --main README.md --op _site --exclude={Gemfile,Rakefile,"coverage/*","vendor/*","bin/*","test/*","tmp/*"} cp -r doc _site/doc - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v2 # Deployment job deploy: diff --git a/.rubocop.yml b/.rubocop.yml index c1c17001..2142296f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,7 +7,7 @@ AllCops: SuggestExtensions: false TargetRubyVersion: 2.7 Exclude: - - '{.git,.github,bin,coverage,pkg,sorbet,spec,test/fixtures,vendor,tmp}/**/*' + - '{.git,.github,.ruby-lsp,bin,coverage,doc,pkg,sorbet,spec,test/fixtures,vendor,tmp}/**/*' - test.rb Gemspec/DevelopmentDependencies: @@ -154,6 +154,9 @@ Style/ParallelAssignment: Style/PerlBackrefs: Enabled: false +Style/RedundantArrayConstructor: + Enabled: false + Style/SafeNavigation: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 273d4003..1beac42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [6.2.0] - 2023-09-20 + +### Added + +- Fix `WithScope` for destructured post arguments. + +### Changed + +- Always use `do`/`end` for multi-line lambdas. + ## [6.1.1] - 2023-03-21 ### Changed @@ -603,7 +613,10 @@ 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.2...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.2.0...HEAD +[6.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.1.1...v6.2.0 +[6.1.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.1.0...v6.1.1 +[6.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.2...v6.1.0 [6.0.2]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.1...v6.0.2 [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 diff --git a/Gemfile.lock b/Gemfile.lock index ad2aeaa5..a9fc60b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,35 +1,41 @@ PATH remote: . specs: - syntax_tree (6.1.1) + syntax_tree (6.2.0) prettier_print (>= 1.2.0) GEM remote: https://rubygems.org/ specs: ast (2.4.2) + base64 (0.1.1) docile (1.4.0) json (2.6.3) - minitest (5.18.0) - parallel (1.22.1) - parser (3.2.1.1) + language_server-protocol (3.17.0.3) + minitest (5.20.0) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) + racc prettier_print (1.2.1) + racc (1.7.1) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.7.0) - rexml (3.2.5) - rubocop (1.48.1) + regexp_parser (2.8.1) + rexml (3.2.6) + rubocop (1.56.3) + base64 (~> 0.1.1) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.26.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.27.0) + rubocop-ast (1.29.0) parser (>= 3.2.1.0) ruby-progressbar (1.13.0) simplecov (0.22.0) diff --git a/README.md b/README.md index 3a3f7d2d..6e1119df 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ Note that the output of the `match` CLI command creates a valid pattern that can ### write -This command will format the listed files and write that formatted version back to the source files. Note that this overwrites the original content, to be sure to be using a version control system. +This command will format the listed files and write that formatted version back to the source files. Note that this overwrites the original content, so be sure to be using a version control system. ```sh stree write path/to/file.rb @@ -525,7 +525,7 @@ With visitors, you only define handlers for the nodes that you need. You can fin * call `visit(child)` with each child that you want to visit * call nothing if you're sure you don't want to descend further -There are a couple of visitors that ship with Syntax Tree that can be used as examples. They live in the [lib/syntax_tree/visitor](lib/syntax_tree/visitor) directory. +There are a couple of visitors that ship with Syntax Tree that can be used as examples. They live in the [lib/syntax_tree](lib/syntax_tree) directory. ### visit_method diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 24d8426f..6c595db5 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -21,6 +21,7 @@ module SyntaxTree # CLI. Requiring those features takes time, so we autoload as many constants # as possible in order to keep the CLI as fast as possible. + autoload :Database, "syntax_tree/database" autoload :DSL, "syntax_tree/dsl" autoload :FieldVisitor, "syntax_tree/field_visitor" autoload :Index, "syntax_tree/index" diff --git a/lib/syntax_tree/database.rb b/lib/syntax_tree/database.rb new file mode 100644 index 00000000..c9981f35 --- /dev/null +++ b/lib/syntax_tree/database.rb @@ -0,0 +1,331 @@ +# frozen_string_literal: true + +module SyntaxTree + # Provides the ability to index source files into a database, then query for + # the nodes. + module Database + class IndexingVisitor < SyntaxTree::FieldVisitor + attr_reader :database, :filepath, :node_id + + def initialize(database, filepath) + @database = database + @filepath = filepath + @node_id = nil + end + + private + + def comments(node) + end + + def field(name, value) + return unless value.is_a?(SyntaxTree::Node) + + binds = [node_id, visit(value), name] + database.execute(<<~SQL, binds) + INSERT INTO edges (from_id, to_id, name) + VALUES (?, ?, ?) + SQL + end + + def list(name, values) + values.each_with_index do |value, index| + binds = [node_id, visit(value), name, index] + database.execute(<<~SQL, binds) + INSERT INTO edges (from_id, to_id, name, list_index) + VALUES (?, ?, ?, ?) + SQL + end + end + + def node(node, _name) + previous = node_id + binds = [ + node.class.name.delete_prefix("SyntaxTree::"), + filepath, + node.location.start_line, + node.location.start_column + ] + + database.execute(<<~SQL, binds) + INSERT INTO nodes (type, path, line, column) + VALUES (?, ?, ?, ?) + SQL + + begin + @node_id = database.last_insert_row_id + yield + @node_id + ensure + @node_id = previous + end + end + + def text(name, value) + end + + def pairs(name, values) + values.each_with_index do |(key, value), index| + binds = [node_id, visit(key), "#{name}[0]", index] + database.execute(<<~SQL, binds) + INSERT INTO edges (from_id, to_id, name, list_index) + VALUES (?, ?, ?, ?) + SQL + + binds = [node_id, visit(value), "#{name}[1]", index] + database.execute(<<~SQL, binds) + INSERT INTO edges (from_id, to_id, name, list_index) + VALUES (?, ?, ?, ?) + SQL + end + end + end + + # Query for a specific type of node. + class TypeQuery + attr_reader :type + + def initialize(type) + @type = type + end + + def each(database, &block) + sql = "SELECT * FROM nodes WHERE type = ?" + database.execute(sql, type).each(&block) + end + end + + # Query for the attributes of a node, optionally also filtering by type. + class AttrQuery + attr_reader :type, :attrs + + def initialize(type, attrs) + @type = type + @attrs = attrs + end + + def each(database, &block) + joins = [] + binds = [] + + attrs.each do |name, query| + ids = query.each(database).map { |row| row[0] } + joins << <<~SQL + JOIN edges AS #{name} + ON #{name}.from_id = nodes.id + AND #{name}.name = ? + AND #{name}.to_id IN (#{(["?"] * ids.size).join(", ")}) + SQL + + binds.push(name).concat(ids) + end + + sql = +"SELECT nodes.* FROM nodes, edges #{joins.join(" ")}" + + if type + sql << " WHERE nodes.type = ?" + binds << type + end + + sql << " GROUP BY nodes.id" + database.execute(sql, binds).each(&block) + end + end + + # Query for the results of either query. + class OrQuery + attr_reader :left, :right + + def initialize(left, right) + @left = left + @right = right + end + + def each(database, &block) + left.each(database, &block) + right.each(database, &block) + end + end + + # A lazy query result. + class QueryResult + attr_reader :database, :query + + def initialize(database, query) + @database = database + @query = query + end + + def each(&block) + return enum_for(__method__) unless block_given? + query.each(database, &block) + end + end + + # A pattern matching expression that will be compiled into a query. + class Pattern + class CompilationError < StandardError + end + + attr_reader :query + + def initialize(query) + @query = query + end + + def compile + program = + begin + SyntaxTree.parse("case nil\nin #{query}\nend") + rescue Parser::ParseError + raise CompilationError, query + end + + compile_node(program.statements.body.first.consequent.pattern) + end + + private + + def compile_error(node) + raise CompilationError, PP.pp(node, +"").chomp + end + + # Shortcut for combining two queries into one that returns the results of + # if either query matches. + def combine_or(left, right) + OrQuery.new(left, right) + end + + # in foo | bar + def compile_binary(node) + compile_error(node) if node.operator != :| + + combine_or(compile_node(node.left), compile_node(node.right)) + end + + # in Ident + def compile_const(node) + value = node.value + + if SyntaxTree.const_defined?(value, false) + clazz = SyntaxTree.const_get(value) + TypeQuery.new(clazz.name.delete_prefix("SyntaxTree::")) + else + compile_error(node) + end + end + + # in SyntaxTree::Ident + def compile_const_path_ref(node) + parent = node.parent + if !parent.is_a?(SyntaxTree::VarRef) || + !parent.value.is_a?(SyntaxTree::Const) + compile_error(node) + end + + if parent.value.value == "SyntaxTree" + compile_node(node.constant) + else + compile_error(node) + end + end + + # in Ident[value: String] + def compile_hshptn(node) + compile_error(node) unless node.keyword_rest.nil? + + attrs = {} + node.keywords.each do |keyword, value| + compile_error(node) unless keyword.is_a?(SyntaxTree::Label) + attrs[keyword.value.chomp(":")] = compile_node(value) + end + + type = node.constant ? compile_node(node.constant).type : nil + AttrQuery.new(type, attrs) + end + + # in Foo + def compile_var_ref(node) + value = node.value + + if value.is_a?(SyntaxTree::Const) + compile_node(value) + else + compile_error(node) + end + end + + def compile_node(node) + case node + when SyntaxTree::Binary + compile_binary(node) + when SyntaxTree::Const + compile_const(node) + when SyntaxTree::ConstPathRef + compile_const_path_ref(node) + when SyntaxTree::HshPtn + compile_hshptn(node) + when SyntaxTree::VarRef + compile_var_ref(node) + else + compile_error(node) + end + end + end + + class Connection + attr_reader :raw_connection + + def initialize(raw_connection) + @raw_connection = raw_connection + end + + def execute(query, binds = []) + raw_connection.execute(query, binds) + end + + def index_file(filepath) + program = SyntaxTree.parse(SyntaxTree.read(filepath)) + program.accept(IndexingVisitor.new(self, filepath)) + end + + def last_insert_row_id + raw_connection.last_insert_row_id + end + + def prepare + raw_connection.execute(<<~SQL) + CREATE TABLE nodes ( + id integer primary key, + type varchar(20), + path varchar(200), + line integer, + column integer + ); + SQL + + raw_connection.execute(<<~SQL) + CREATE INDEX nodes_type ON nodes (type); + SQL + + raw_connection.execute(<<~SQL) + CREATE TABLE edges ( + id integer primary key, + from_id integer, + to_id integer, + name varchar(20), + list_index integer + ); + SQL + + raw_connection.execute(<<~SQL) + CREATE INDEX edges_name ON edges (name); + SQL + end + + def search(query) + QueryResult.new(self, Pattern.new(query).compile) + end + end + end +end diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index 60858bf2..2b229885 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -60,7 +60,7 @@ def initialize( # constant. That constant is responsible for determining the default # disable ternary value. If it's defined, then we default to true. # Otherwise we default to false. - defined?(DISABLE_TERNARY) + defined?(DISABLE_AUTO_TERNARY) else disable_auto_ternary end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 54d132e6..3b676552 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -1299,7 +1299,7 @@ def format(q) end end - # [nil | VarRef] the optional constant wrapper + # [nil | VarRef | ConstPathRef] the optional constant wrapper attr_reader :constant # [Array[ Node ]] the regular positional arguments that this array @@ -2849,7 +2849,10 @@ def format_chain(q, children) # to print the operator trailing in order to keep it working. last_child = children.last if last_child.is_a?(CallNode) && last_child.message != :call && - last_child.message.comments.any? && last_child.operator + ( + (last_child.message.comments.any? && last_child.operator) || + (last_child.operator && last_child.operator.comments.any?) + ) q.format(CallOperatorFormatter.new(last_child.operator)) skip_operator = true else @@ -5413,7 +5416,7 @@ def ===(other) # end # class FndPtn < Node - # [nil | Node] the optional constant wrapper + # [nil | VarRef | ConstPathRef] the optional constant wrapper attr_reader :constant # [VarField] the splat on the left-hand side @@ -6035,7 +6038,7 @@ def format(q) end end - # [nil | Node] the optional constant wrapper + # [nil | VarRef | ConstPathRef] the optional constant wrapper attr_reader :constant # [Array[ [DynaSymbol | Label, nil | Node] ]] the set of tuples @@ -7207,36 +7210,17 @@ def format(q) q.text(" ") q .if_break do - force_parens = - q.parents.any? do |node| - node.is_a?(Command) || node.is_a?(CommandCall) - end - - if force_parens - q.text("{") + q.text("do") - unless statements.empty? - q.indent do - q.breakable_space - q.format(statements) - end + unless statements.empty? + q.indent do q.breakable_space + q.format(statements) end - - q.text("}") - else - q.text("do") - - unless statements.empty? - q.indent do - q.breakable_space - q.format(statements) - end - end - - q.breakable_space - q.text("end") end + + q.breakable_space + q.text("end") end .if_flat do q.text("{") @@ -8293,8 +8277,8 @@ def format(q) # parameter attr_reader :rest - # [Array[ Ident ]] any positional parameters that exist after a rest - # parameter + # [Array[ Ident | MLHSParen ]] any positional parameters that exist after a + # rest parameter attr_reader :posts # [Array[ [ Label, nil | Node ] ]] any keyword parameters and their diff --git a/lib/syntax_tree/plugin/disable_ternary.rb b/lib/syntax_tree/plugin/disable_auto_ternary.rb similarity index 70% rename from lib/syntax_tree/plugin/disable_ternary.rb rename to lib/syntax_tree/plugin/disable_auto_ternary.rb index 0cb48d84..dd38c783 100644 --- a/lib/syntax_tree/plugin/disable_ternary.rb +++ b/lib/syntax_tree/plugin/disable_auto_ternary.rb @@ -2,6 +2,6 @@ module SyntaxTree class Formatter - DISABLE_TERNARY = true + DISABLE_AUTO_TERNARY = true end end diff --git a/lib/syntax_tree/reflection.rb b/lib/syntax_tree/reflection.rb index aa7b85b6..6955aa21 100644 --- a/lib/syntax_tree/reflection.rb +++ b/lib/syntax_tree/reflection.rb @@ -64,7 +64,7 @@ def inspect class << self def parse(comment) - comment = comment.gsub(/\n/, " ") + comment = comment.gsub("\n", " ") unless comment.start_with?("[") raise "Comment does not start with a bracket: #{comment.inspect}" diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index ad87d3e1..51599f77 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.1.1" + VERSION = "6.2.0" end diff --git a/lib/syntax_tree/with_scope.rb b/lib/syntax_tree/with_scope.rb index c479fd3e..8c4908f3 100644 --- a/lib/syntax_tree/with_scope.rb +++ b/lib/syntax_tree/with_scope.rb @@ -152,10 +152,7 @@ def visit_def(node) # arguments. def visit_params(node) add_argument_definitions(node.requireds) - - node.posts.each do |param| - current_scope.add_local_definition(param, :argument) - end + add_argument_definitions(node.posts) node.keywords.each do |param| current_scope.add_local_definition(param.first, :argument) diff --git a/lib/syntax_tree/yarv/assembler.rb b/lib/syntax_tree/yarv/assembler.rb index ac400506..b29c252a 100644 --- a/lib/syntax_tree/yarv/assembler.rb +++ b/lib/syntax_tree/yarv/assembler.rb @@ -31,7 +31,6 @@ def visit_string_literal(node) "FCALL" => CallData::CALL_FCALL, "VCALL" => CallData::CALL_VCALL, "ARGS_SIMPLE" => CallData::CALL_ARGS_SIMPLE, - "BLOCKISEQ" => CallData::CALL_BLOCKISEQ, "KWARG" => CallData::CALL_KWARG, "KW_SPLAT" => CallData::CALL_KW_SPLAT, "TAILCALL" => CallData::CALL_TAILCALL, diff --git a/lib/syntax_tree/yarv/calldata.rb b/lib/syntax_tree/yarv/calldata.rb index fadea61b..e35992f5 100644 --- a/lib/syntax_tree/yarv/calldata.rb +++ b/lib/syntax_tree/yarv/calldata.rb @@ -5,19 +5,26 @@ module YARV # This is an operand to various YARV instructions that represents the # information about a specific call site. class CallData - CALL_ARGS_SPLAT = 1 << 0 - CALL_ARGS_BLOCKARG = 1 << 1 - CALL_FCALL = 1 << 2 - CALL_VCALL = 1 << 3 - CALL_ARGS_SIMPLE = 1 << 4 - CALL_BLOCKISEQ = 1 << 5 - CALL_KWARG = 1 << 6 - CALL_KW_SPLAT = 1 << 7 - CALL_TAILCALL = 1 << 8 - CALL_SUPER = 1 << 9 - CALL_ZSUPER = 1 << 10 - CALL_OPT_SEND = 1 << 11 - CALL_KW_SPLAT_MUT = 1 << 12 + flags = %i[ + CALL_ARGS_SPLAT + CALL_ARGS_BLOCKARG + CALL_FCALL + CALL_VCALL + CALL_ARGS_SIMPLE + CALL_KWARG + CALL_KW_SPLAT + CALL_TAILCALL + CALL_SUPER + CALL_ZSUPER + CALL_OPT_SEND + CALL_KW_SPLAT_MUT + ] + + # Insert the legacy CALL_BLOCKISEQ flag for Ruby 3.2 and earlier. + flags.insert(5, :CALL_BLOCKISEQ) if RUBY_VERSION < "3.3" + + # Set the flags as constants on the class. + flags.each_with_index { |name, index| const_set(name, 1 << index) } attr_reader :method, :argc, :flags, :kw_arg @@ -50,7 +57,6 @@ def inspect names << :FCALL if flag?(CALL_FCALL) names << :VCALL if flag?(CALL_VCALL) names << :ARGS_SIMPLE if flag?(CALL_ARGS_SIMPLE) - names << :BLOCKISEQ if flag?(CALL_BLOCKISEQ) names << :KWARG if flag?(CALL_KWARG) names << :KW_SPLAT if flag?(CALL_KW_SPLAT) names << :TAILCALL if flag?(CALL_TAILCALL) diff --git a/lib/syntax_tree/yarv/instruction_sequence.rb b/lib/syntax_tree/yarv/instruction_sequence.rb index 7ce7bcdd..df92799b 100644 --- a/lib/syntax_tree/yarv/instruction_sequence.rb +++ b/lib/syntax_tree/yarv/instruction_sequence.rb @@ -353,11 +353,27 @@ def specialize_instructions! next unless calldata.argc == 0 case calldata.method + when :min + node.value = + if RUBY_VERSION < "3.3" + Legacy::OptNewArrayMin.new(value.number) + else + OptNewArraySend.new(value.number, :min) + end + + node.next_node = next_node.next_node when :max - node.value = OptNewArrayMax.new(value.number) + node.value = + if RUBY_VERSION < "3.3" + Legacy::OptNewArrayMax.new(value.number) + else + OptNewArraySend.new(value.number, :max) + end + node.next_node = next_node.next_node - when :min - node.value = OptNewArrayMin.new(value.number) + when :hash + next if RUBY_VERSION < "3.3" + node.value = OptNewArraySend.new(value.number, :hash) node.next_node = next_node.next_node end when PutObject, PutString @@ -1174,6 +1190,9 @@ def self.from(source, options = Compiler::Options.new, parent_iseq = nil) when :opt_newarray_min iseq.newarray(opnds[0]) iseq.send(YARV.calldata(:min)) + when :opt_newarray_send + iseq.newarray(opnds[0]) + iseq.send(CallData.new(opnds[1])) when :opt_neq iseq.push( OptNEq.new(CallData.from(opnds[0]), CallData.from(opnds[1])) diff --git a/lib/syntax_tree/yarv/instructions.rb b/lib/syntax_tree/yarv/instructions.rb index ceb237dc..ffeebe65 100644 --- a/lib/syntax_tree/yarv/instructions.rb +++ b/lib/syntax_tree/yarv/instructions.rb @@ -3818,9 +3818,10 @@ def call(vm) # ### Summary # - # `opt_newarray_max` is a specialization that occurs when the `max` method - # is called on an array literal. It pops the values of the array off the - # stack and pushes on the result. + # `opt_newarray_send` is a specialization that occurs when a dynamic array + # literal is created and immediately sent the `min`, `max`, or `hash` + # methods. It pops the values of the array off the stack and pushes on the + # result of the method call. # # ### Usage # @@ -3828,83 +3829,36 @@ def call(vm) # [a, b, c].max # ~~~ # - class OptNewArrayMax < Instruction - attr_reader :number - - def initialize(number) - @number = number - end - - def disasm(fmt) - fmt.instruction("opt_newarray_max", [fmt.object(number)]) - end - - def to_a(_iseq) - [:opt_newarray_max, number] - end - - def deconstruct_keys(_keys) - { number: number } - end - - def ==(other) - other.is_a?(OptNewArrayMax) && other.number == number - end - - def length - 2 - end - - def pops - number - end + class OptNewArraySend < Instruction + attr_reader :number, :method - def pushes - 1 - end - - def call(vm) - vm.push(vm.pop(number).max) - end - end - - # ### Summary - # - # `opt_newarray_min` is a specialization that occurs when the `min` method - # is called on an array literal. It pops the values of the array off the - # stack and pushes on the result. - # - # ### Usage - # - # ~~~ruby - # [a, b, c].min - # ~~~ - # - class OptNewArrayMin < Instruction - attr_reader :number - - def initialize(number) + def initialize(number, method) @number = number + @method = method end def disasm(fmt) - fmt.instruction("opt_newarray_min", [fmt.object(number)]) + fmt.instruction( + "opt_newarray_send", + [fmt.object(number), fmt.object(method)] + ) end def to_a(_iseq) - [:opt_newarray_min, number] + [:opt_newarray_send, number, method] end def deconstruct_keys(_keys) - { number: number } + { number: number, method: method } end def ==(other) - other.is_a?(OptNewArrayMin) && other.number == number + other.is_a?(OptNewArraySend) && other.number == number && + other.method == method end def length - 2 + 3 end def pops @@ -3916,7 +3870,7 @@ def pushes end def call(vm) - vm.push(vm.pop(number).min) + vm.push(vm.pop(number).__send__(method)) end end diff --git a/lib/syntax_tree/yarv/legacy.rb b/lib/syntax_tree/yarv/legacy.rb index e20729d9..8715993a 100644 --- a/lib/syntax_tree/yarv/legacy.rb +++ b/lib/syntax_tree/yarv/legacy.rb @@ -124,6 +124,110 @@ def falls_through? end end + # ### Summary + # + # `opt_newarray_max` is a specialization that occurs when the `max` method + # is called on an array literal. It pops the values of the array off the + # stack and pushes on the result. + # + # ### Usage + # + # ~~~ruby + # [a, b, c].max + # ~~~ + # + class OptNewArrayMax < Instruction + attr_reader :number + + def initialize(number) + @number = number + end + + def disasm(fmt) + fmt.instruction("opt_newarray_max", [fmt.object(number)]) + end + + def to_a(_iseq) + [:opt_newarray_max, number] + end + + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(OptNewArrayMax) && other.number == number + end + + def length + 2 + end + + def pops + number + end + + def pushes + 1 + end + + def call(vm) + vm.push(vm.pop(number).max) + end + end + + # ### Summary + # + # `opt_newarray_min` is a specialization that occurs when the `min` method + # is called on an array literal. It pops the values of the array off the + # stack and pushes on the result. + # + # ### Usage + # + # ~~~ruby + # [a, b, c].min + # ~~~ + # + class OptNewArrayMin < Instruction + attr_reader :number + + def initialize(number) + @number = number + end + + def disasm(fmt) + fmt.instruction("opt_newarray_min", [fmt.object(number)]) + end + + def to_a(_iseq) + [:opt_newarray_min, number] + end + + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(OptNewArrayMin) && other.number == number + end + + def length + 2 + end + + def pops + number + end + + def pushes + 1 + end + + def call(vm) + vm.push(vm.pop(number).min) + end + end + # ### Summary # # `opt_setinlinecache` sets an inline cache for a constant lookup. It pops diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 1922f8c6..ca3e8dde 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -311,6 +311,12 @@ class CompilerTest < Minitest::Test "[1, 2, 3].min", "[foo, bar, baz].min", "[foo, bar, baz].min(1)", + "[1, 2, 3].hash", + "[foo, bar, baz].hash", + "[foo, bar, baz].hash(1)", + "[1, 2, 3].foo", + "[foo, bar, baz].foo", + "[foo, bar, baz].foo(1)", "[**{ x: true }][0][:x]", # Core method calls "alias foo bar", diff --git a/test/fixtures/lambda.rb b/test/fixtures/lambda.rb index 5dba3be3..8b922ef0 100644 --- a/test/fixtures/lambda.rb +++ b/test/fixtures/lambda.rb @@ -80,3 +80,31 @@ -> do # comment1 # comment2 end +% # multiline lambda in a command +command "arg" do + -> { + multi + line + } +end +- +command "arg" do + -> do + multi + line + end +end +% # multiline lambda in a command call +command.call "arg" do + -> { + multi + line + } +end +- +command.call "arg" do + -> do + multi + line + end +end diff --git a/test/plugin/disable_ternary_test.rb b/test/plugin/disable_auto_ternary_test.rb similarity index 100% rename from test/plugin/disable_ternary_test.rb rename to test/plugin/disable_auto_ternary_test.rb diff --git a/test/with_scope_test.rb b/test/with_scope_test.rb index 5bf276be..6b48d17d 100644 --- a/test/with_scope_test.rb +++ b/test/with_scope_test.rb @@ -154,6 +154,42 @@ def foo(a) assert_argument(collector, "a", definitions: [1], usages: [2]) end + def test_collecting_methods_with_destructured_post_arguments + collector = Collector.collect(<<~RUBY) + def foo(optional = 1, (bin, bag)) + end + RUBY + + assert_equal(3, collector.arguments.length) + assert_argument(collector, "optional", definitions: [1], usages: []) + assert_argument(collector, "bin", definitions: [1], usages: []) + assert_argument(collector, "bag", definitions: [1], usages: []) + end + + def test_collecting_methods_with_desctructured_post_using_splat + collector = Collector.collect(<<~RUBY) + def foo(optional = 1, (bin, bag, *)) + end + RUBY + + assert_equal(3, collector.arguments.length) + assert_argument(collector, "optional", definitions: [1], usages: []) + assert_argument(collector, "bin", definitions: [1], usages: []) + assert_argument(collector, "bag", definitions: [1], usages: []) + end + + def test_collecting_methods_with_nested_desctructured + collector = Collector.collect(<<~RUBY) + def foo(optional = 1, (bin, (bag))) + end + RUBY + + assert_equal(3, collector.arguments.length) + assert_argument(collector, "optional", definitions: [1], usages: []) + assert_argument(collector, "bin", definitions: [1], usages: []) + assert_argument(collector, "bag", definitions: [1], usages: []) + end + def test_collecting_singleton_method_arguments collector = Collector.collect(<<~RUBY) def self.foo(a)