diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 514ac27a..e54c9100 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.5 + uses: dependabot/fetch-metadata@v1.3.6 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 fc02f2fe..6c64676d 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -27,7 +27,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Setup Pages - uses: actions/configure-pages@v2 + uses: actions/configure-pages@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.gitmodules b/.gitmodules index f5477ea3..1a2c45cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "spec"] path = spec/ruby url = git@github.com:ruby/spec.git +[submodule "test/ruby-syntax-fixtures"] + path = test/ruby-syntax-fixtures + url = https://github.com/ruby-syntax-tree/ruby-syntax-fixtures diff --git a/.rubocop.yml b/.rubocop.yml index 069041bd..bc98a43a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,8 +8,12 @@ AllCops: TargetRubyVersion: 2.7 Exclude: - '{.git,.github,bin,coverage,pkg,spec,test/fixtures,vendor,tmp}/**/*' + - test/ruby-syntax-fixtures/**/* - test.rb +Gemspec/DevelopmentDependencies: + Enabled: false + Layout/LineLength: Max: 80 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b29fcbb..c39bed36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [5.3.0] - 2023-01-26 + +### Added + +- `#arity` has been added to `DefNode`, `BlockNode`, and `Params`. The method returns a range where the lower bound is the minimum and the upper bound is the maximum number of arguments that can be used to invoke that block/method definition. +- `#arity` has been added to `CallNode`, `Command`, `CommandCall`, and `VCall` nodes. The method returns the number of arguments included in the invocation. For splats, double splats, or argument forwards, this method returns `Float::INFINITY`. +- `SyntaxTree::index` and `SyntaxTree::index_file` APIs have been added to collect a list of classes, modules, and methods defined in a given source string or file, respectively. These APIs are experimental and subject to change. +- A `plugin/disable_auto_ternary` plugin has been added the disables the formatted that automatically changes permissable `if/else` clauses into ternaries. + +### Changed + +- Files are now only written from the CLI if the content of them changes, which should match watching files less chaotic. +- In the case that `rb_iseq_load` cannot be found, `Fiddle::DLError` is now rescued. +- Previously if there were invalid UTF-8 byte sequences after the `__END__` keyword the parser could potentially have crashed when parsing comments. This has been fixed. +- Previously there was special formatting for array literals that contained only variable references (either locals, method calls, or constants). For consistency, this has been removed and all array literals are now formatted the same way. + ## [5.2.0] - 2023-01-04 ### Added @@ -481,7 +497,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/v5.2.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.3.0...HEAD +[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 [5.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.0.1...v5.1.0 [5.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.0.0...v5.0.1 diff --git a/Gemfile.lock b/Gemfile.lock index bb5e3663..799bd891 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (5.2.0) + syntax_tree (5.3.0) prettier_print (>= 1.2.0) GEM @@ -17,18 +17,18 @@ GEM prettier_print (1.2.0) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.6.1) + regexp_parser (2.6.2) rexml (3.2.5) - rubocop (1.42.0) + rubocop (1.44.1) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + 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) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) + unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.24.1) parser (>= 3.1.1.0) ruby-progressbar (1.11.0) @@ -38,7 +38,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - unicode-display_width (2.4.1) + unicode-display_width (2.4.2) PLATFORMS arm64-darwin-21 diff --git a/README.md b/README.md index 7a943ca8..3c437947 100644 --- a/README.md +++ b/README.md @@ -658,6 +658,7 @@ To register plugins, define a file somewhere in your load path named `syntax_tre * `plugin/single_quotes` - This will change all of your string literals to use single quotes instead of the default double quotes. * `plugin/trailing_comma` - This will put trailing commas into multiline array literals, hash literals, and method calls that can support trailing commas. +* `plugin/disable_auto_ternary` - This will prevent the automatic conversion of `if ... else` to ternary expressions. If you're using Syntax Tree as a library, you can require those files directly or manually pass those options to the formatter initializer through the `SyntaxTree::Formatter::Options` class. diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index f1217ac3..f5c71aba 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -26,6 +26,7 @@ require_relative "syntax_tree/parser" require_relative "syntax_tree/pattern" require_relative "syntax_tree/search" +require_relative "syntax_tree/index" require_relative "syntax_tree/yarv" require_relative "syntax_tree/yarv/bf" @@ -116,4 +117,18 @@ def self.read(filepath) def self.search(source, query, &block) Search.new(Pattern.new(query).compile).scan(parse(source), &block) end + + # Indexes the given source code to return a list of all class, module, and + # method definitions. Used to quickly provide indexing capability for IDEs or + # documentation generation. + def self.index(source) + Index.index(source) + end + + # Indexes the given file to return a list of all class, module, and method + # definitions. Used to quickly provide indexing capability for IDEs or + # documentation generation. + def self.index_file(filepath) + Index.index_file(filepath) + end end diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 392dd627..7e6f4067 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -303,10 +303,11 @@ def run(item) options.print_width, options: options.formatter_options ) + changed = source != formatted - File.write(filepath, formatted) if item.writable? + File.write(filepath, formatted) if item.writable? && changed - color = source == formatted ? Color.gray(filepath) : filepath + color = changed ? filepath : Color.gray(filepath) delta = ((Time.now - start) * 1000).round puts "#{color} #{delta}ms" diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index fddc06fe..c64cf7d1 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -21,11 +21,15 @@ def initialize(version) # that folks have become entrenched in their ways, we decided to provide a # small amount of configurability. class Options - attr_reader :quote, :trailing_comma, :target_ruby_version + attr_reader :quote, + :trailing_comma, + :disable_auto_ternary, + :target_ruby_version def initialize( quote: :default, trailing_comma: :default, + disable_auto_ternary: :default, target_ruby_version: :default ) @quote = @@ -50,6 +54,17 @@ def initialize( trailing_comma end + @disable_auto_ternary = + if disable_auto_ternary == :default + # We ship with a disable ternary plugin that will define this + # 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) + else + disable_auto_ternary + end + @target_ruby_version = if target_ruby_version == :default # The default target Ruby version is the current version of Ruby. @@ -69,8 +84,13 @@ def initialize( # These options are overridden in plugins to we need to make sure they are # available here. - attr_reader :quote, :trailing_comma, :target_ruby_version + attr_reader :quote, + :trailing_comma, + :disable_auto_ternary, + :target_ruby_version + alias trailing_comma? trailing_comma + alias disable_auto_ternary? disable_auto_ternary def initialize(source, *args, options: Options.new) super(*args) @@ -81,6 +101,7 @@ def initialize(source, *args, options: Options.new) # Memoizing these values to make access faster. @quote = options.quote @trailing_comma = options.trailing_comma + @disable_auto_ternary = options.disable_auto_ternary @target_ruby_version = options.target_ruby_version end diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb new file mode 100644 index 00000000..8b33f785 --- /dev/null +++ b/lib/syntax_tree/index.rb @@ -0,0 +1,374 @@ +# frozen_string_literal: true + +module SyntaxTree + # This class can be used to build an index of the structure of Ruby files. We + # define an index as the list of constants and methods defined within a file. + # + # This index strives to be as fast as possible to better support tools like + # IDEs. Because of that, it has different backends depending on what + # functionality is available. + module Index + # This is a location for an index entry. + class Location + attr_reader :line, :column + + def initialize(line, column) + @line = line + @column = column + end + end + + # This entry represents a class definition using the class keyword. + class ClassDefinition + attr_reader :nesting, :name, :location, :comments + + def initialize(nesting, name, location, comments) + @nesting = nesting + @name = name + @location = location + @comments = comments + end + end + + # This entry represents a module definition using the module keyword. + class ModuleDefinition + attr_reader :nesting, :name, :location, :comments + + def initialize(nesting, name, location, comments) + @nesting = nesting + @name = name + @location = location + @comments = comments + end + end + + # This entry represents a method definition using the def keyword. + class MethodDefinition + attr_reader :nesting, :name, :location, :comments + + def initialize(nesting, name, location, comments) + @nesting = nesting + @name = name + @location = location + @comments = comments + end + end + + # This entry represents a singleton method definition using the def keyword + # with a specified target. + class SingletonMethodDefinition + attr_reader :nesting, :name, :location, :comments + + def initialize(nesting, name, location, comments) + @nesting = nesting + @name = name + @location = location + @comments = comments + end + end + + # When you're using the instruction sequence backend, this class is used to + # lazily parse comments out of the source code. + class FileComments + # We use the ripper library to pull out source comments. + class Parser < Ripper + attr_reader :comments + + def initialize(*) + super + @comments = {} + end + + def on_comment(value) + comments[lineno] = value.chomp + end + end + + # This represents the Ruby source in the form of a file. When it needs to + # be read we'll read the file. + class FileSource + attr_reader :filepath + + def initialize(filepath) + @filepath = filepath + end + + def source + File.read(filepath) + end + end + + # This represents the Ruby source in the form of a string. When it needs + # to be read the string is returned. + class StringSource + attr_reader :source + + def initialize(source) + @source = source + end + end + + attr_reader :source + + def initialize(source) + @source = source + end + + def comments + @comments ||= Parser.new(source.source).tap(&:parse).comments + end + end + + # This class handles parsing comments from Ruby source code in the case that + # we use the instruction sequence backend. Because the instruction sequence + # backend doesn't provide comments (since they are dropped) we provide this + # interface to lazily parse them out. + class EntryComments + include Enumerable + attr_reader :file_comments, :location + + def initialize(file_comments, location) + @file_comments = file_comments + @location = location + end + + def each(&block) + line = location.line - 1 + result = [] + + while line >= 0 && (comment = file_comments.comments[line]) + result.unshift(comment) + line -= 1 + end + + result.each(&block) + end + end + + # This backend creates the index using RubyVM::InstructionSequence, which is + # faster than using the Syntax Tree parser, but is not available on all + # runtimes. + class ISeqBackend + VM_DEFINECLASS_TYPE_CLASS = 0x00 + VM_DEFINECLASS_TYPE_SINGLETON_CLASS = 0x01 + VM_DEFINECLASS_TYPE_MODULE = 0x02 + VM_DEFINECLASS_FLAG_SCOPED = 0x08 + VM_DEFINECLASS_FLAG_HAS_SUPERCLASS = 0x10 + + def index(source) + index_iseq( + RubyVM::InstructionSequence.compile(source).to_a, + FileComments.new(FileComments::StringSource.new(source)) + ) + end + + def index_file(filepath) + index_iseq( + RubyVM::InstructionSequence.compile_file(filepath).to_a, + FileComments.new(FileComments::FileSource.new(filepath)) + ) + end + + private + + def location_for(iseq) + code_location = iseq[4][:code_location] + Location.new(code_location[0], code_location[1]) + 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) + + case insn[0] + when :defineclass + _, name, class_iseq, flags = insn + + 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] + 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, + name, + location, + EntryComments.new(file_comments, location) + ) + else + location = location_for(class_iseq) + results << ClassDefinition.new( + current_nesting, + name, + location, + EntryComments.new(file_comments, location) + ) + end + + queue << [class_iseq, current_nesting + [name]] + when :definemethod + location = location_for(insn[2]) + results << MethodDefinition.new( + current_nesting, + insn[1], + location, + EntryComments.new(file_comments, location) + ) + when :definesmethod + if current_iseq[13][index - 1] != [:putself] + raise NotImplementedError, + "singleton method with non-self receiver" + end + + location = location_for(insn[2]) + results << SingletonMethodDefinition.new( + current_nesting, + insn[1], + location, + EntryComments.new(file_comments, location) + ) + end + end + end + + results + end + end + + # This backend creates the index using the Syntax Tree parser and a visitor. + # It is not as fast as using the instruction sequences directly, but is + # supported on all runtimes. + class ParserBackend + class IndexVisitor < Visitor + attr_reader :results, :nesting, :statements + + def initialize + @results = [] + @nesting = [] + @statements = nil + end + + def visit_class(node) + name = visit(node.constant).to_sym + location = + Location.new(node.location.start_line, node.location.start_column) + + results << ClassDefinition.new( + nesting.dup, + name, + location, + comments_for(node) + ) + + nesting << name + super + nesting.pop + end + + def visit_const_ref(node) + node.constant.value + end + + def visit_def(node) + name = node.name.value.to_sym + location = + Location.new(node.location.start_line, node.location.start_column) + + results << if node.target.nil? + MethodDefinition.new( + nesting.dup, + name, + location, + comments_for(node) + ) + else + SingletonMethodDefinition.new( + nesting.dup, + name, + location, + comments_for(node) + ) + end + end + + def visit_module(node) + name = visit(node.constant).to_sym + location = + Location.new(node.location.start_line, node.location.start_column) + + results << ModuleDefinition.new( + nesting.dup, + name, + location, + comments_for(node) + ) + + nesting << name + super + nesting.pop + end + + def visit_program(node) + super + results + end + + def visit_statements(node) + @statements = node + super + end + + private + + def comments_for(node) + comments = [] + + body = statements.body + line = node.location.start_line - 1 + index = body.index(node) - 1 + + while index >= 0 && body[index].is_a?(Comment) && + (line - body[index].location.start_line < 2) + comments.unshift(body[index].value) + line = body[index].location.start_line + index -= 1 + end + + comments + end + end + + def index(source) + SyntaxTree.parse(source).accept(IndexVisitor.new) + end + + def index_file(filepath) + index(SyntaxTree.read(filepath)) + end + end + + # The class defined here is used to perform the indexing, depending on what + # functionality is available from the runtime. + INDEX_BACKEND = + defined?(RubyVM::InstructionSequence) ? ISeqBackend : ParserBackend + + # This method accepts source code and then indexes it. + def self.index(source, backend: INDEX_BACKEND.new) + backend.index(source) + end + + # This method accepts a filepath and then indexes it. + def self.index_file(filepath, backend: INDEX_BACKEND.new) + backend.index_file(filepath) + end + end +end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index f19cfb2c..fc5517cf 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -775,6 +775,10 @@ def ===(other) other.is_a?(ArgParen) && arguments === other.arguments end + def arity + arguments&.arity || 0 + end + private def trailing_comma? @@ -848,6 +852,21 @@ def format(q) def ===(other) other.is_a?(Args) && ArrayMatch.call(parts, other.parts) end + + def arity + parts.sum do |part| + case part + when ArgStar, ArgsForward + Float::INFINITY + when BareAssocHash + part.assocs.sum do |assoc| + assoc.is_a?(AssocSplat) ? Float::INFINITY : 1 + end + else + 1 + end + end + end end # ArgBlock represents using a block operator on an expression. @@ -1008,6 +1027,10 @@ def format(q) def ===(other) other.is_a?(ArgsForward) end + + def arity + Float::INFINITY + end end # ArrayLiteral represents an array literal, which can optionally contain @@ -1080,58 +1103,6 @@ def format(q) end end - # Formats an array that contains only a list of variable references. To make - # things simpler, if there are a bunch, we format them all using the "fill" - # algorithm as opposed to breaking them into a ton of lines. For example, - # - # [foo, bar, baz] - # - # instead of becoming: - # - # [ - # foo, - # bar, - # baz - # ] - # - # would instead become: - # - # [ - # foo, bar, - # baz - # ] - # - # provided the line length was hit between `bar` and `baz`. - class VarRefsFormatter - # The separator for the fill algorithm. - class Separator - def call(q) - q.text(",") - q.fill_breakable - end - end - - # [Args] the contents of the array - attr_reader :contents - - def initialize(contents) - @contents = contents - end - - def format(q) - q.text("[") - q.group do - q.indent do - q.breakable_empty - q.seplist(contents.parts, Separator.new) { |part| q.format(part) } - q.if_break { q.text(",") } if q.trailing_comma? - end - q.breakable_empty - end - q.text("]") - end - end - # This is a special formatter used if the array literal contains no values # but _does_ contain comments. In this case we do some special formatting to # make sure the comments gets indented properly. @@ -1206,19 +1177,17 @@ def deconstruct_keys(_keys) end def format(q) - if qwords? - QWordsFormatter.new(contents).format(q) - return - end - - if qsymbols? - QSymbolsFormatter.new(contents).format(q) - return - end + if lbracket.comments.empty? && contents && contents.comments.empty? && + contents.parts.length > 1 + if qwords? + QWordsFormatter.new(contents).format(q) + return + end - if var_refs?(q) - VarRefsFormatter.new(contents).format(q) - return + if qsymbols? + QSymbolsFormatter.new(contents).format(q) + return + end end if empty_with_comments? @@ -1250,39 +1219,24 @@ def ===(other) private def qwords? - lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.length > 1 && - contents.parts.all? do |part| - case part - when StringLiteral - part.comments.empty? && part.parts.length == 1 && - part.parts.first.is_a?(TStringContent) && - !part.parts.first.value.match?(/[\s\[\]\\]/) - when CHAR - !part.value.match?(/[\[\]\\]/) - else - false - end + contents.parts.all? do |part| + case part + when StringLiteral + part.comments.empty? && part.parts.length == 1 && + part.parts.first.is_a?(TStringContent) && + !part.parts.first.value.match?(/[\s\[\]\\]/) + when CHAR + !part.value.match?(/[\[\]\\]/) + else + false end + end end def qsymbols? - lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.length > 1 && - contents.parts.all? do |part| - part.is_a?(SymbolLiteral) && part.comments.empty? - end - end - - def var_refs?(q) - lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.all? do |part| - part.is_a?(VarRef) && part.comments.empty? - end && - ( - contents.parts.sum { |part| part.value.value.length + 2 } > - q.maxwidth * 2 - ) + contents.parts.all? do |part| + part.is_a?(SymbolLiteral) && part.comments.empty? + end end # If we have an empty array that contains only comments, then we're going @@ -3068,6 +3022,10 @@ def format_contents(q) end end end + + def arity + arguments&.arity || 0 + end end # Case represents the beginning of a case chain. @@ -3481,6 +3439,10 @@ def ===(other) arguments === other.arguments && block === other.block end + def arity + arguments.arity + end + private def align(q, node, &block) @@ -3646,6 +3608,10 @@ def ===(other) arguments === other.arguments && block === other.block end + def arity + arguments&.arity || 0 + end + private def argument_alignment(q, doc) @@ -4175,6 +4141,17 @@ def ===(other) def endless? !bodystmt.is_a?(BodyStmt) end + + def arity + case params + when Params + params.arity + when Paren + params.contents.arity + else + 0..0 + end + end end # Defined represents the use of the +defined?+ operator. It can be used with @@ -4362,6 +4339,15 @@ def keywords? opening.is_a?(Kw) end + def arity + case block_var + when BlockVar + block_var.params.arity + else + 0..0 + end + end + private # If this is nested anywhere inside certain nodes, then we can't change @@ -6153,7 +6139,7 @@ def self.call(parent) module Ternaryable class << self def call(q, node) - return false if ENV["STREE_FAST_FORMAT"] + return false if ENV["STREE_FAST_FORMAT"] || q.disable_auto_ternary? # If this is a conditional inside of a parentheses as the only content, # then we don't want to transform it into a ternary. Presumably the user @@ -6496,9 +6482,26 @@ def deconstruct_keys(_keys) def format(q) force_flat = [ - AliasNode, Assign, Break, Command, CommandCall, Heredoc, IfNode, IfOp, - Lambda, MAssign, Next, OpAssign, RescueMod, ReturnNode, Super, Undef, - UnlessNode, VoidStmt, YieldNode, ZSuper + AliasNode, + Assign, + Break, + Command, + CommandCall, + Heredoc, + IfNode, + IfOp, + Lambda, + MAssign, + Next, + OpAssign, + RescueMod, + ReturnNode, + Super, + Undef, + UnlessNode, + VoidStmt, + YieldNode, + ZSuper ] if q.parent.is_a?(Paren) || force_flat.include?(truthy.class) || @@ -8325,6 +8328,29 @@ def ===(other) keyword_rest === other.keyword_rest && block === other.block end + # Returns a range representing the possible number of arguments accepted + # by this params node not including the block. For example: + # + # def foo(a, b = 1, c:, d: 2, &block) + # ... + # end + # + # has arity 2..4. + # + def arity + optional_keywords = keywords.count { |_label, value| value } + + lower_bound = + requireds.length + posts.length + keywords.length - optional_keywords + + upper_bound = + if keyword_rest.nil? && rest.nil? + lower_bound + optionals.length + optional_keywords + end + + lower_bound..upper_bound + end + private def format_contents(q, parts) @@ -11594,6 +11620,10 @@ def ===(other) def access_control? @access_control ||= %w[private protected public].include?(value.value) end + + def arity + 0 + end end # VoidStmt represents an empty lexical block of code. diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 602bb98f..99b703d0 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -1103,6 +1103,7 @@ def on_command_call(receiver, operator, message, arguments) # :call-seq: # on_comment: (String value) -> Comment def on_comment(value) + # char is the index of the # character in the source. char = char_pos location = Location.token( @@ -1112,8 +1113,24 @@ def on_comment(value) size: value.size - 1 ) - index = source.rindex(/[^\t ]/, char - 1) if char != 0 - inline = index && (source[index] != "\n") + # Loop backward in the source string, starting from the beginning of the + # comment, and find the first character that is not a space or a tab. If + # index is -1, this indicates that we've checked all of the characters + # back to the start of the source, so this comment must be at the + # beginning of the file. + # + # We are purposefully not using rindex or regular expressions here because + # they check if there are invalid characters, which is actually possible + # with the use of __END__ syntax. + index = char - 1 + while index > -1 && (source[index] == "\t" || source[index] == " ") + index -= 1 + end + + # If we found a character that was not a space or a tab before the comment + # and it's a newline, then this comment is inline. Otherwise, it stands on + # its own and can be attached as its own node in the tree. + inline = index != -1 && source[index] != "\n" comment = Comment.new(value: value.chomp, inline: inline, location: location) diff --git a/lib/syntax_tree/plugin/disable_ternary.rb b/lib/syntax_tree/plugin/disable_ternary.rb new file mode 100644 index 00000000..0cb48d84 --- /dev/null +++ b/lib/syntax_tree/plugin/disable_ternary.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SyntaxTree + class Formatter + DISABLE_TERNARY = true + end +end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index a97f5e43..6cb1fccf 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "5.2.0" + VERSION = "5.3.0" end diff --git a/lib/syntax_tree/yarv/assembler.rb b/lib/syntax_tree/yarv/assembler.rb index ec467b58..ac400506 100644 --- a/lib/syntax_tree/yarv/assembler.rb +++ b/lib/syntax_tree/yarv/assembler.rb @@ -62,22 +62,26 @@ def visit_string_literal(node) "constant-from" ].freeze - attr_reader :filepath + attr_reader :lines - def initialize(filepath) - @filepath = filepath + def initialize(lines) + @lines = lines end def assemble iseq = InstructionSequence.new("
", "", 1, :top) - assemble_iseq(iseq, File.readlines(filepath, chomp: true)) + assemble_iseq(iseq, lines) iseq.compile! iseq end - def self.assemble(filepath) - new(filepath).assemble + def self.assemble(source) + new(source.lines(chomp: true)).assemble + end + + def self.assemble_file(filepath) + new(File.readlines(filepath, chomp: true)).assemble end private diff --git a/lib/syntax_tree/yarv/compiler.rb b/lib/syntax_tree/yarv/compiler.rb index 4c9a4d50..c1b4d6dd 100644 --- a/lib/syntax_tree/yarv/compiler.rb +++ b/lib/syntax_tree/yarv/compiler.rb @@ -285,7 +285,7 @@ def visit_unsupported(_node) # if we need to return the value of the last statement. attr_reader :last_statement - def initialize(options) + def initialize(options = Options.new) @options = options @iseq = nil @last_statement = false diff --git a/lib/syntax_tree/yarv/decompiler.rb b/lib/syntax_tree/yarv/decompiler.rb index 47d2a2df..753ba80a 100644 --- a/lib/syntax_tree/yarv/decompiler.rb +++ b/lib/syntax_tree/yarv/decompiler.rb @@ -97,7 +97,7 @@ def decompile(iseq) clause << Next(Args([])) when Leave value = Args([clause.pop]) - clause << (iseq.type == :top ? Break(value) : ReturnNode(value)) + clause << (iseq.type != :top ? Break(value) : ReturnNode(value)) when OptAnd, OptDiv, OptEq, OptGE, OptGT, OptLE, OptLT, OptLTLT, OptMinus, OptMod, OptMult, OptOr, OptPlus left, right = clause.pop(2) diff --git a/lib/syntax_tree/yarv/instruction_sequence.rb b/lib/syntax_tree/yarv/instruction_sequence.rb index c284221b..6aa7279e 100644 --- a/lib/syntax_tree/yarv/instruction_sequence.rb +++ b/lib/syntax_tree/yarv/instruction_sequence.rb @@ -70,7 +70,7 @@ def push(instruction) [Fiddle::TYPE_VOIDP] * 3, Fiddle::TYPE_VOIDP ) - rescue NameError + rescue NameError, Fiddle::DLError end # This object is used to track the size of the stack at any given time. It diff --git a/lib/syntax_tree/yarv/instructions.rb b/lib/syntax_tree/yarv/instructions.rb index 5e1d116b..bba06f8d 100644 --- a/lib/syntax_tree/yarv/instructions.rb +++ b/lib/syntax_tree/yarv/instructions.rb @@ -91,6 +91,14 @@ def to_a(_iseq) [:adjuststack, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(AdjustStack) && other.number == number + end + def length 2 end @@ -139,6 +147,14 @@ def to_a(_iseq) [:anytostring] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(AnyToString) + end + def length 1 end @@ -197,6 +213,14 @@ def to_a(_iseq) [:branchif, label.name] end + def deconstruct_keys(_keys) + { label: label } + end + + def ==(other) + other.is_a?(BranchIf) && other.label == label + end + def length 2 end @@ -250,6 +274,14 @@ def to_a(_iseq) [:branchnil, label.name] end + def deconstruct_keys(_keys) + { label: label } + end + + def ==(other) + other.is_a?(BranchNil) && other.label == label + end + def length 2 end @@ -302,6 +334,14 @@ def to_a(_iseq) [:branchunless, label.name] end + def deconstruct_keys(_keys) + { label: label } + end + + def ==(other) + other.is_a?(BranchUnless) && other.label == label + end + def length 2 end @@ -365,6 +405,16 @@ def to_a(iseq) ] end + def deconstruct_keys(_keys) + { keyword_bits_index: keyword_bits_index, keyword_index: keyword_index } + end + + def ==(other) + other.is_a?(CheckKeyword) && + other.keyword_bits_index == keyword_bits_index && + other.keyword_index == keyword_index + end + def length 3 end @@ -419,6 +469,14 @@ def to_a(_iseq) [:checkmatch, type] end + def deconstruct_keys(_keys) + { type: type } + end + + def ==(other) + other.is_a?(CheckMatch) && other.type == type + end + def length 2 end @@ -561,6 +619,14 @@ def to_a(_iseq) [:checktype, type] end + def deconstruct_keys(_keys) + { type: type } + end + + def ==(other) + other.is_a?(CheckType) && other.type == type + end + def length 2 end @@ -656,6 +722,14 @@ def to_a(_iseq) [:concatarray] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(ConcatArray) + end + def length 1 end @@ -708,6 +782,14 @@ def to_a(_iseq) [:concatstrings, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(ConcatStrings) && other.number == number + end + def length 2 end @@ -771,6 +853,15 @@ def to_a(_iseq) [:defineclass, name, class_iseq.to_a, flags] end + def deconstruct_keys(_keys) + { name: name, class_iseq: class_iseq, flags: flags } + end + + def ==(other) + other.is_a?(DefineClass) && other.name == name && + other.class_iseq == class_iseq && other.flags == flags + end + def length 4 end @@ -899,6 +990,15 @@ def to_a(_iseq) [:defined, type, name, message] end + def deconstruct_keys(_keys) + { type: type, name: name, message: message } + end + + def ==(other) + other.is_a?(Defined) && other.type == type && other.name == name && + other.message == message + end + def length 4 end @@ -989,6 +1089,15 @@ def to_a(_iseq) [:definemethod, method_name, method_iseq.to_a] end + def deconstruct_keys(_keys) + { method_name: method_name, method_iseq: method_iseq } + end + + def ==(other) + other.is_a?(DefineMethod) && other.method_name == method_name && + other.method_iseq == method_iseq + end + def length 3 end @@ -1061,6 +1170,15 @@ def to_a(_iseq) [:definesmethod, method_name, method_iseq.to_a] end + def deconstruct_keys(_keys) + { method_name: method_name, method_iseq: method_iseq } + end + + def ==(other) + other.is_a?(DefineSMethod) && other.method_name == method_name && + other.method_iseq == method_iseq + end + def length 3 end @@ -1118,6 +1236,14 @@ def to_a(_iseq) [:dup] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(Dup) + end + def length 1 end @@ -1164,6 +1290,14 @@ def to_a(_iseq) [:duparray, object] end + def deconstruct_keys(_keys) + { object: object } + end + + def ==(other) + other.is_a?(DupArray) && other.object == object + end + def length 2 end @@ -1210,6 +1344,14 @@ def to_a(_iseq) [:duphash, object] end + def deconstruct_keys(_keys) + { object: object } + end + + def ==(other) + other.is_a?(DupHash) && other.object == object + end + def length 2 end @@ -1256,6 +1398,14 @@ def to_a(_iseq) [:dupn, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(DupN) && other.number == number + end + def length 2 end @@ -1307,6 +1457,15 @@ def to_a(_iseq) [:expandarray, number, flags] end + def deconstruct_keys(_keys) + { number: number, flags: flags } + end + + def ==(other) + other.is_a?(ExpandArray) && other.number == number && + other.flags == flags + end + def length 3 end @@ -1398,6 +1557,15 @@ def to_a(iseq) [:getblockparam, current.local_table.offset(index), level] end + def deconstruct_keys(_keys) + { index: index, level: level } + end + + def ==(other) + other.is_a?(GetBlockParam) && other.index == index && + other.level == level + end + def length 3 end @@ -1455,6 +1623,15 @@ def to_a(iseq) [:getblockparamproxy, current.local_table.offset(index), level] end + def deconstruct_keys(_keys) + { index: index, level: level } + end + + def ==(other) + other.is_a?(GetBlockParamProxy) && other.index == index && + other.level == level + end + def length 3 end @@ -1507,6 +1684,15 @@ def to_a(_iseq) [:getclassvariable, name, cache] end + def deconstruct_keys(_keys) + { name: name, cache: cache } + end + + def ==(other) + other.is_a?(GetClassVariable) && other.name == name && + other.cache == cache + end + def length 3 end @@ -1557,6 +1743,14 @@ def to_a(_iseq) [:getconstant, name] end + def deconstruct_keys(_keys) + { name: name } + end + + def ==(other) + other.is_a?(GetConstant) && other.name == name + end + def length 2 end @@ -1619,6 +1813,14 @@ def to_a(_iseq) [:getglobal, name] end + def deconstruct_keys(_keys) + { name: name } + end + + def ==(other) + other.is_a?(GetGlobal) && other.name == name + end + def length 2 end @@ -1678,6 +1880,15 @@ def to_a(_iseq) [:getinstancevariable, name, cache] end + def deconstruct_keys(_keys) + { name: name, cache: cache } + end + + def ==(other) + other.is_a?(GetInstanceVariable) && other.name == name && + other.cache == cache + end + def length 3 end @@ -1732,6 +1943,14 @@ def to_a(iseq) [:getlocal, current.local_table.offset(index), level] end + def deconstruct_keys(_keys) + { index: index, level: level } + end + + def ==(other) + other.is_a?(GetLocal) && other.index == index && other.level == level + end + def length 3 end @@ -1781,6 +2000,14 @@ def to_a(iseq) [:getlocal_WC_0, iseq.local_table.offset(index)] end + def deconstruct_keys(_keys) + { index: index } + end + + def ==(other) + other.is_a?(GetLocalWC0) && other.index == index + end + def length 2 end @@ -1830,6 +2057,14 @@ def to_a(iseq) [:getlocal_WC_1, iseq.parent_iseq.local_table.offset(index)] end + def deconstruct_keys(_keys) + { index: index } + end + + def ==(other) + other.is_a?(GetLocalWC1) && other.index == index + end + def length 2 end @@ -1881,6 +2116,14 @@ def to_a(_iseq) [:getspecial, key, type] end + def deconstruct_keys(_keys) + { key: key, type: type } + end + + def ==(other) + other.is_a?(GetSpecial) && other.key == key && other.type == type + end + def length 3 end @@ -1929,6 +2172,14 @@ def to_a(_iseq) [:intern] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(Intern) + end + def length 1 end @@ -1979,6 +2230,14 @@ def to_a(_iseq) [:invokeblock, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(InvokeBlock) && other.calldata == calldata + end + def length 2 end @@ -2034,6 +2293,15 @@ def to_a(_iseq) [:invokesuper, calldata.to_h, block_iseq&.to_a] end + def deconstruct_keys(_keys) + { calldata: calldata, block_iseq: block_iseq } + end + + def ==(other) + other.is_a?(InvokeSuper) && other.calldata == calldata && + other.block_iseq == block_iseq + end + def length 1 end @@ -2105,6 +2373,14 @@ def to_a(_iseq) [:jump, label.name] end + def deconstruct_keys(_keys) + { label: label } + end + + def ==(other) + other.is_a?(Jump) && other.label == label + end + def length 2 end @@ -2145,6 +2421,14 @@ def to_a(_iseq) [:leave] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(Leave) + end + def length 1 end @@ -2195,6 +2479,14 @@ def to_a(_iseq) [:newarray, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(NewArray) && other.number == number + end + def length 2 end @@ -2243,6 +2535,14 @@ def to_a(_iseq) [:newarraykwsplat, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(NewArrayKwSplat) && other.number == number + end + def length 2 end @@ -2293,6 +2593,14 @@ def to_a(_iseq) [:newhash, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(NewHash) && other.number == number + end + def length 2 end @@ -2344,6 +2652,14 @@ def to_a(_iseq) [:newrange, exclude_end] end + def deconstruct_keys(_keys) + { exclude_end: exclude_end } + end + + def ==(other) + other.is_a?(NewRange) && other.exclude_end == exclude_end + end + def length 2 end @@ -2385,6 +2701,14 @@ def to_a(_iseq) [:nop] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(Nop) + end + def length 1 end @@ -2434,6 +2758,14 @@ def to_a(_iseq) [:objtostring, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(ObjToString) && other.calldata == calldata + end + def length 2 end @@ -2485,6 +2817,14 @@ def to_a(_iseq) [:once, iseq.to_a, cache] end + def deconstruct_keys(_keys) + { iseq: iseq, cache: cache } + end + + def ==(other) + other.is_a?(Once) && other.iseq == iseq && other.cache == cache + end + def length 3 end @@ -2536,6 +2876,14 @@ def to_a(_iseq) [:opt_and, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptAnd) && other.calldata == calldata + end + def length 2 end @@ -2584,6 +2932,14 @@ def to_a(_iseq) [:opt_aref, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptAref) && other.calldata == calldata + end + def length 2 end @@ -2637,6 +2993,15 @@ def to_a(_iseq) [:opt_aref_with, object, calldata.to_h] end + def deconstruct_keys(_keys) + { object: object, calldata: calldata } + end + + def ==(other) + other.is_a?(OptArefWith) && other.object == object && + other.calldata == calldata + end + def length 3 end @@ -2686,6 +3051,14 @@ def to_a(_iseq) [:opt_aset, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptAset) && other.calldata == calldata + end + def length 2 end @@ -2738,6 +3111,15 @@ def to_a(_iseq) [:opt_aset_with, object, calldata.to_h] end + def deconstruct_keys(_keys) + { object: object, calldata: calldata } + end + + def ==(other) + other.is_a?(OptAsetWith) && other.object == object && + other.calldata == calldata + end + def length 3 end @@ -2806,6 +3188,16 @@ def to_a(_iseq) ] end + def deconstruct_keys(_keys) + { case_dispatch_hash: case_dispatch_hash, else_label: else_label } + end + + def ==(other) + other.is_a?(OptCaseDispatch) && + other.case_dispatch_hash == case_dispatch_hash && + other.else_label == else_label + end + def length 3 end @@ -2855,6 +3247,14 @@ def to_a(_iseq) [:opt_div, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptDiv) && other.calldata == calldata + end + def length 2 end @@ -2903,6 +3303,14 @@ def to_a(_iseq) [:opt_empty_p, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptEmptyP) && other.calldata == calldata + end + def length 2 end @@ -2952,6 +3360,14 @@ def to_a(_iseq) [:opt_eq, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptEq) && other.calldata == calldata + end + def length 2 end @@ -3001,6 +3417,14 @@ def to_a(_iseq) [:opt_ge, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptGE) && other.calldata == calldata + end + def length 2 end @@ -3050,6 +3474,14 @@ def to_a(_iseq) [:opt_getconstant_path, names] end + def deconstruct_keys(_keys) + { names: names } + end + + def ==(other) + other.is_a?(OptGetConstantPath) && other.names == names + end + def length 2 end @@ -3106,6 +3538,14 @@ def to_a(_iseq) [:opt_gt, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptGT) && other.calldata == calldata + end + def length 2 end @@ -3155,6 +3595,14 @@ def to_a(_iseq) [:opt_le, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptLE) && other.calldata == calldata + end + def length 2 end @@ -3204,6 +3652,14 @@ def to_a(_iseq) [:opt_length, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptLength) && other.calldata == calldata + end + def length 2 end @@ -3253,6 +3709,14 @@ def to_a(_iseq) [:opt_lt, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptLT) && other.calldata == calldata + end + def length 2 end @@ -3302,6 +3766,14 @@ def to_a(_iseq) [:opt_ltlt, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptLTLT) && other.calldata == calldata + end + def length 2 end @@ -3352,6 +3824,14 @@ def to_a(_iseq) [:opt_minus, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptMinus) && other.calldata == calldata + end + def length 2 end @@ -3401,6 +3881,14 @@ def to_a(_iseq) [:opt_mod, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptMod) && other.calldata == calldata + end + def length 2 end @@ -3450,6 +3938,14 @@ def to_a(_iseq) [:opt_mult, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptMult) && other.calldata == calldata + end + def length 2 end @@ -3505,6 +4001,15 @@ def to_a(_iseq) [:opt_neq, eq_calldata.to_h, neq_calldata.to_h] end + def deconstruct_keys(_keys) + { eq_calldata: eq_calldata, neq_calldata: neq_calldata } + end + + def ==(other) + other.is_a?(OptNEq) && other.eq_calldata == eq_calldata && + other.neq_calldata == neq_calldata + end + def length 3 end @@ -3554,6 +4059,14 @@ 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 @@ -3602,6 +4115,14 @@ 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 @@ -3651,6 +4172,14 @@ def to_a(_iseq) [:opt_nil_p, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptNilP) && other.calldata == calldata + end + def length 2 end @@ -3698,6 +4227,14 @@ def to_a(_iseq) [:opt_not, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptNot) && other.calldata == calldata + end + def length 2 end @@ -3747,6 +4284,14 @@ def to_a(_iseq) [:opt_or, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptOr) && other.calldata == calldata + end + def length 2 end @@ -3796,6 +4341,14 @@ def to_a(_iseq) [:opt_plus, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptPlus) && other.calldata == calldata + end + def length 2 end @@ -3844,6 +4397,14 @@ def to_a(_iseq) [:opt_regexpmatch2, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptRegExpMatch2) && other.calldata == calldata + end + def length 2 end @@ -3892,6 +4453,14 @@ def to_a(_iseq) [:opt_send_without_block, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptSendWithoutBlock) && other.calldata == calldata + end + def length 2 end @@ -3941,6 +4510,14 @@ def to_a(_iseq) [:opt_size, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptSize) && other.calldata == calldata + end + def length 2 end @@ -3993,6 +4570,15 @@ def to_a(_iseq) [:opt_str_freeze, object, calldata.to_h] end + def deconstruct_keys(_keys) + { object: object, calldata: calldata } + end + + def ==(other) + other.is_a?(OptStrFreeze) && other.object == object && + other.calldata == calldata + end + def length 3 end @@ -4045,6 +4631,15 @@ def to_a(_iseq) [:opt_str_uminus, object, calldata.to_h] end + def deconstruct_keys(_keys) + { object: object, calldata: calldata } + end + + def ==(other) + other.is_a?(OptStrUMinus) && other.object == object && + other.calldata == calldata + end + def length 3 end @@ -4094,6 +4689,14 @@ def to_a(_iseq) [:opt_succ, calldata.to_h] end + def deconstruct_keys(_keys) + { calldata: calldata } + end + + def ==(other) + other.is_a?(OptSucc) && other.calldata == calldata + end + def length 2 end @@ -4134,6 +4737,14 @@ def to_a(_iseq) [:pop] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(Pop) + end + def length 1 end @@ -4174,6 +4785,14 @@ def to_a(_iseq) [:putnil] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(PutNil) + end + def length 1 end @@ -4220,6 +4839,14 @@ def to_a(_iseq) [:putobject, object] end + def deconstruct_keys(_keys) + { object: object } + end + + def ==(other) + other.is_a?(PutObject) && other.object == object + end + def length 2 end @@ -4262,6 +4889,14 @@ def to_a(_iseq) [:putobject_INT2FIX_0_] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(PutObjectInt2Fix0) + end + def length 1 end @@ -4304,6 +4939,14 @@ def to_a(_iseq) [:putobject_INT2FIX_1_] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(PutObjectInt2Fix1) + end + def length 1 end @@ -4344,6 +4987,14 @@ def to_a(_iseq) [:putself] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(PutSelf) + end + def length 1 end @@ -4396,6 +5047,14 @@ def to_a(_iseq) [:putspecialobject, object] end + def deconstruct_keys(_keys) + { object: object } + end + + def ==(other) + other.is_a?(PutSpecialObject) && other.object == object + end + def length 2 end @@ -4451,6 +5110,14 @@ def to_a(_iseq) [:putstring, object] end + def deconstruct_keys(_keys) + { object: object } + end + + def ==(other) + other.is_a?(PutString) && other.object == object + end + def length 2 end @@ -4505,6 +5172,15 @@ def to_a(_iseq) [:send, calldata.to_h, block_iseq&.to_a] end + def deconstruct_keys(_keys) + { calldata: calldata, block_iseq: block_iseq } + end + + def ==(other) + other.is_a?(Send) && other.calldata == calldata && + other.block_iseq == block_iseq + end + def length 3 end @@ -4582,6 +5258,15 @@ def to_a(iseq) [:setblockparam, current.local_table.offset(index), level] end + def deconstruct_keys(_keys) + { index: index, level: level } + end + + def ==(other) + other.is_a?(SetBlockParam) && other.index == index && + other.level == level + end + def length 3 end @@ -4635,6 +5320,15 @@ def to_a(_iseq) [:setclassvariable, name, cache] end + def deconstruct_keys(_keys) + { name: name, cache: cache } + end + + def ==(other) + other.is_a?(SetClassVariable) && other.name == name && + other.cache == cache + end + def length 3 end @@ -4684,6 +5378,14 @@ def to_a(_iseq) [:setconstant, name] end + def deconstruct_keys(_keys) + { name: name } + end + + def ==(other) + other.is_a?(SetConstant) && other.name == name + end + def length 2 end @@ -4732,6 +5434,14 @@ def to_a(_iseq) [:setglobal, name] end + def deconstruct_keys(_keys) + { name: name } + end + + def ==(other) + other.is_a?(SetGlobal) && other.name == name + end + def length 2 end @@ -4790,6 +5500,15 @@ def to_a(_iseq) [:setinstancevariable, name, cache] end + def deconstruct_keys(_keys) + { name: name, cache: cache } + end + + def ==(other) + other.is_a?(SetInstanceVariable) && other.name == name && + other.cache == cache + end + def length 3 end @@ -4844,6 +5563,14 @@ def to_a(iseq) [:setlocal, current.local_table.offset(index), level] end + def deconstruct_keys(_keys) + { index: index, level: level } + end + + def ==(other) + other.is_a?(SetLocal) && other.index == index && other.level == level + end + def length 3 end @@ -4893,6 +5620,14 @@ def to_a(iseq) [:setlocal_WC_0, iseq.local_table.offset(index)] end + def deconstruct_keys(_keys) + { index: index } + end + + def ==(other) + other.is_a?(SetLocalWC0) && other.index == index + end + def length 2 end @@ -4942,6 +5677,14 @@ def to_a(iseq) [:setlocal_WC_1, iseq.parent_iseq.local_table.offset(index)] end + def deconstruct_keys(_keys) + { index: index } + end + + def ==(other) + other.is_a?(SetLocalWC1) && other.index == index + end + def length 2 end @@ -4989,6 +5732,14 @@ def to_a(_iseq) [:setn, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(SetN) && other.number == number + end + def length 2 end @@ -5037,6 +5788,14 @@ def to_a(_iseq) [:setspecial, key] end + def deconstruct_keys(_keys) + { key: key } + end + + def ==(other) + other.is_a?(SetSpecial) && other.key == key + end + def length 2 end @@ -5092,6 +5851,14 @@ def to_a(_iseq) [:splatarray, flag] end + def deconstruct_keys(_keys) + { flag: flag } + end + + def ==(other) + other.is_a?(SplatArray) && other.flag == flag + end + def length 2 end @@ -5156,6 +5923,14 @@ def to_a(_iseq) [:swap] end + def deconstruct_keys(_keys) + {} + end + + def ==(other) + other.is_a?(Swap) + end + def length 1 end @@ -5218,6 +5993,14 @@ def to_a(_iseq) [:throw, type] end + def deconstruct_keys(_keys) + { type: type } + end + + def ==(other) + other.is_a?(Throw) && other.type == type + end + def length 2 end @@ -5304,6 +6087,14 @@ def to_a(_iseq) [:topn, number] end + def deconstruct_keys(_keys) + { number: number } + end + + def ==(other) + other.is_a?(TopN) && other.number == number + end + def length 2 end @@ -5352,6 +6143,15 @@ def to_a(_iseq) [:toregexp, options, length] end + def deconstruct_keys(_keys) + { options: options, length: length } + end + + def ==(other) + other.is_a?(ToRegExp) && other.options == options && + other.length == length + end + def pops length end diff --git a/lib/syntax_tree/yarv/legacy.rb b/lib/syntax_tree/yarv/legacy.rb index b2e33290..ab9b00df 100644 --- a/lib/syntax_tree/yarv/legacy.rb +++ b/lib/syntax_tree/yarv/legacy.rb @@ -34,6 +34,14 @@ def to_a(_iseq) [:getclassvariable, name] end + def deconstruct_keys(_keys) + { name: name } + end + + def ==(other) + other.is_a?(GetClassVariable) && other.name == name + end + def length 2 end @@ -90,6 +98,15 @@ def to_a(_iseq) [:opt_getinlinecache, label.name, cache] end + def deconstruct_keys(_keys) + { label: label, cache: cache } + end + + def ==(other) + other.is_a?(OptGetInlineCache) && other.label == label && + other.cache == cache + end + def length 3 end @@ -141,6 +158,14 @@ def to_a(_iseq) [:opt_setinlinecache, cache] end + def deconstruct_keys(_keys) + { cache: cache } + end + + def ==(other) + other.is_a?(OptSetInlineCache) && other.cache == cache + end + def length 2 end @@ -190,6 +215,14 @@ def to_a(_iseq) [:setclassvariable, name] end + def deconstruct_keys(_keys) + { name: name } + end + + def ==(other) + other.is_a?(SetClassVariable) && other.name == name + end + def length 2 end diff --git a/lib/syntax_tree/yarv/vm.rb b/lib/syntax_tree/yarv/vm.rb index 1bbb82ed..b303944d 100644 --- a/lib/syntax_tree/yarv/vm.rb +++ b/lib/syntax_tree/yarv/vm.rb @@ -219,6 +219,10 @@ def initialize(events = NullEvents.new) @frame = nil end + def self.run(iseq) + new.run_top_frame(iseq) + end + ########################################################################## # Helper methods for frames ########################################################################## diff --git a/test/fixtures/array_literal.rb b/test/fixtures/array_literal.rb index df807728..391d2eae 100644 --- a/test/fixtures/array_literal.rb +++ b/test/fixtures/array_literal.rb @@ -24,9 +24,16 @@ - fooooooooooooooooo = 1 [ - fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, - fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, - fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo, + fooooooooooooooooo ] % [ diff --git a/test/index_test.rb b/test/index_test.rb new file mode 100644 index 00000000..6bb83881 --- /dev/null +++ b/test/index_test.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class IndexTest < Minitest::Test + def test_module + index_each("module Foo; end") do |entry| + assert_equal :Foo, entry.name + assert_empty 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 + end + end + + def test_module_comments + index_each("# comment1\n# comment2\nmodule Foo; end") do |entry| + assert_equal :Foo, entry.name + assert_equal ["# comment1", "# comment2"], entry.comments.to_a + end + end + + def test_class + index_each("class Foo; end") do |entry| + assert_equal :Foo, entry.name + assert_empty 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 + end + end + + def test_class_comments + index_each("# comment1\n# comment2\nclass Foo; end") do |entry| + assert_equal :Foo, entry.name + assert_equal ["# comment1", "# comment2"], entry.comments.to_a + end + end + + def test_method + index_each("def foo; end") do |entry| + assert_equal :foo, entry.name + assert_empty entry.nesting + end + end + + def test_method_nested + index_each("class Foo; def foo; end; end") do |entry| + assert_equal :foo, entry.name + assert_equal [:Foo], entry.nesting + end + end + + def test_method_comments + index_each("# comment1\n# comment2\ndef foo; end") do |entry| + assert_equal :foo, entry.name + assert_equal ["# comment1", "# comment2"], entry.comments.to_a + end + end + + def test_singleton_method + index_each("def self.foo; end") do |entry| + assert_equal :foo, entry.name + assert_empty entry.nesting + end + end + + 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 + end + end + + def test_singleton_method_comments + index_each("# comment1\n# comment2\ndef self.foo; end") do |entry| + assert_equal :foo, entry.name + assert_equal ["# comment1", "# comment2"], entry.comments.to_a + end + end + + def test_this_file + entries = Index.index_file(__FILE__, backend: Index::ParserBackend.new) + + if defined?(RubyVM::InstructionSequence) + entries += Index.index_file(__FILE__, backend: Index::ISeqBackend.new) + end + + entries.map { |entry| entry.comments.to_a } + end + + private + + def index_each(source) + yield Index.index(source, backend: Index::ParserBackend.new).last + + if defined?(RubyVM::InstructionSequence) + yield Index.index(source, backend: Index::ISeqBackend.new).last + end + end + end +end diff --git a/test/node_test.rb b/test/node_test.rb index 3d700e73..7254c086 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -1058,6 +1058,342 @@ def test_root_class_raises_not_implemented_errors end end + def test_arity_no_args + source = <<~SOURCE + def foo + end + SOURCE + + at = location(chars: 0..11, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(0..0, node.arity) + node + end + end + + def test_arity_positionals + source = <<~SOURCE + def foo(a, b = 1) + end + SOURCE + + at = location(chars: 0..21, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(1..2, node.arity) + node + end + end + + def test_arity_rest + source = <<~SOURCE + def foo(a, *b) + end + SOURCE + + at = location(chars: 0..18, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(1.., node.arity) + node + end + end + + def test_arity_keyword_rest + source = <<~SOURCE + def foo(a, **b) + end + SOURCE + + at = location(chars: 0..19, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(1.., node.arity) + node + end + end + + def test_arity_keywords + source = <<~SOURCE + def foo(a:, b: 1) + end + SOURCE + + at = location(chars: 0..21, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(1..2, node.arity) + node + end + end + + def test_arity_mixed + source = <<~SOURCE + def foo(a, b = 1, c:, d: 2) + end + SOURCE + + at = location(chars: 0..31, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(2..4, node.arity) + node + end + end + + guard_version("2.7.3") do + def test_arity_arg_forward + source = <<~SOURCE + def foo(...) + end + SOURCE + + at = location(chars: 0..16, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(0.., node.arity) + node + end + end + end + + guard_version("3.0.0") do + def test_arity_positional_and_arg_forward + source = <<~SOURCE + def foo(a, ...) + end + SOURCE + + at = location(chars: 0..19, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(1.., node.arity) + node + end + end + end + + def test_arity_no_parenthesis + source = <<~SOURCE + def foo a, b = 1 + end + SOURCE + + at = location(chars: 0..20, columns: 0..3, lines: 1..2) + assert_node(DefNode, source, at: at) do |node| + assert_equal(1..2, node.arity) + node + end + end + + def test_block_arity_positionals + source = <<~SOURCE + [].each do |a, b, c| + end + SOURCE + + at = location(chars: 8..24, columns: 8..3, lines: 1..2) + assert_node(BlockNode, source, at: at) do |node| + block = node.block + assert_equal(3..3, block.arity) + block + end + end + + def test_block_arity_with_optional + source = <<~SOURCE + [].each do |a, b = 1| + end + SOURCE + + at = location(chars: 8..25, columns: 8..3, lines: 1..2) + assert_node(BlockNode, source, at: at) do |node| + block = node.block + assert_equal(1..2, block.arity) + block + end + end + + def test_block_arity_with_optional_keyword + source = <<~SOURCE + [].each do |a, b: 2| + end + SOURCE + + at = location(chars: 8..24, columns: 8..3, lines: 1..2) + assert_node(BlockNode, source, at: at) do |node| + block = node.block + assert_equal(1..2, block.arity) + block + end + end + + def test_call_node_arity_positional_arguments + source = <<~SOURCE + foo(1, 2, 3) + SOURCE + + at = location(chars: 0..12, columns: 0..3, lines: 1..1) + assert_node(CallNode, source, at: at) do |node| + assert_equal(3, node.arity) + node + end + end + + def test_call_node_arity_keyword_arguments + source = <<~SOURCE + foo(bar, something: 123) + SOURCE + + at = location(chars: 0..24, columns: 0..24, lines: 1..1) + assert_node(CallNode, source, at: at) do |node| + assert_equal(2, node.arity) + node + end + end + + def test_call_node_arity_splat_arguments + source = <<~SOURCE + foo(*bar) + SOURCE + + at = location(chars: 0..9, columns: 0..9, lines: 1..1) + assert_node(CallNode, source, at: at) do |node| + assert_equal(Float::INFINITY, node.arity) + node + end + end + + def test_call_node_arity_keyword_rest_arguments + source = <<~SOURCE + foo(**bar) + SOURCE + + at = location(chars: 0..10, columns: 0..10, lines: 1..1) + assert_node(CallNode, source, at: at) do |node| + assert_equal(Float::INFINITY, node.arity) + node + end + end + + guard_version("2.7.3") do + def test_call_node_arity_arg_forward_arguments + source = <<~SOURCE + def foo(...) + bar(...) + end + SOURCE + + at = location(chars: 15..23, columns: 2..10, lines: 2..2) + assert_node(CallNode, source, at: at) do |node| + call = node.bodystmt.statements.body.first + assert_equal(Float::INFINITY, call.arity) + call + end + end + end + + def test_command_arity_positional_arguments + source = <<~SOURCE + foo 1, 2, 3 + SOURCE + + at = location(chars: 0..11, columns: 0..3, lines: 1..1) + assert_node(Command, source, at: at) do |node| + assert_equal(3, node.arity) + node + end + end + + def test_command_arity_keyword_arguments + source = <<~SOURCE + foo bar, something: 123 + SOURCE + + at = location(chars: 0..23, columns: 0..23, lines: 1..1) + assert_node(Command, source, at: at) do |node| + assert_equal(2, node.arity) + node + end + end + + def test_command_arity_splat_arguments + source = <<~SOURCE + foo *bar + SOURCE + + at = location(chars: 0..8, columns: 0..8, lines: 1..1) + assert_node(Command, source, at: at) do |node| + assert_equal(Float::INFINITY, node.arity) + node + end + end + + def test_command_arity_keyword_rest_arguments + source = <<~SOURCE + foo **bar + SOURCE + + at = location(chars: 0..9, columns: 0..9, lines: 1..1) + assert_node(Command, source, at: at) do |node| + assert_equal(Float::INFINITY, node.arity) + node + end + end + + def test_command_call_arity_positional_arguments + source = <<~SOURCE + object.foo 1, 2, 3 + SOURCE + + at = location(chars: 0..18, columns: 0..3, lines: 1..1) + assert_node(CommandCall, source, at: at) do |node| + assert_equal(3, node.arity) + node + end + end + + def test_command_call_arity_keyword_arguments + source = <<~SOURCE + object.foo bar, something: 123 + SOURCE + + at = location(chars: 0..30, columns: 0..30, lines: 1..1) + assert_node(CommandCall, source, at: at) do |node| + assert_equal(2, node.arity) + node + end + end + + def test_command_call_arity_splat_arguments + source = <<~SOURCE + object.foo *bar + SOURCE + + at = location(chars: 0..15, columns: 0..15, lines: 1..1) + assert_node(CommandCall, source, at: at) do |node| + assert_equal(Float::INFINITY, node.arity) + node + end + end + + def test_command_call_arity_keyword_rest_arguments + source = <<~SOURCE + object.foo **bar + SOURCE + + at = location(chars: 0..16, columns: 0..16, lines: 1..1) + assert_node(CommandCall, source, at: at) do |node| + assert_equal(Float::INFINITY, node.arity) + node + end + end + + def test_vcall_arity + source = <<~SOURCE + foo + SOURCE + + at = location(chars: 0..3, columns: 0..3, lines: 1..1) + assert_node(VCall, source, at: at) do |node| + assert_equal(0, node.arity) + node + end + end + private def location(lines: 1..1, chars: 0..0, columns: 0..0) diff --git a/test/parser_test.rb b/test/parser_test.rb index 6048cf11..8d6c0a16 100644 --- a/test/parser_test.rb +++ b/test/parser_test.rb @@ -65,5 +65,14 @@ def foo end RUBY end + + def test_does_not_choke_on_invalid_characters_in_source_string + SyntaxTree.parse(<<~RUBY) + # comment + # comment + __END__ + \xC5 + RUBY + end end end diff --git a/test/plugin/disable_ternary_test.rb b/test/plugin/disable_ternary_test.rb new file mode 100644 index 00000000..b2af9d35 --- /dev/null +++ b/test/plugin/disable_ternary_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module SyntaxTree + class DisableTernaryTest < Minitest::Test + def test_short_if_else_unchanged + assert_format(<<~RUBY) + if true + 1 + else + 2 + end + RUBY + end + + def test_short_ternary_unchanged + assert_format("true ? 1 : 2\n") + end + + private + + def assert_format(expected, source = expected) + options = Formatter::Options.new(disable_auto_ternary: true) + formatter = Formatter.new(source, [], options: options) + SyntaxTree.parse(source).format(formatter) + + formatter.flush + assert_equal(expected, formatter.output.join) + end + end +end diff --git a/test/ruby-syntax-fixtures b/test/ruby-syntax-fixtures new file mode 160000 index 00000000..5b333f5a --- /dev/null +++ b/test/ruby-syntax-fixtures @@ -0,0 +1 @@ +Subproject commit 5b333f5a34d6fb08f88acc93b69c7d19b3fee8e7 diff --git a/test/ruby_syntax_fixtures_test.rb b/test/ruby_syntax_fixtures_test.rb new file mode 100644 index 00000000..0cf89310 --- /dev/null +++ b/test/ruby_syntax_fixtures_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class RubySyntaxFixturesTest < Minitest::Test + Dir[ + File.expand_path("ruby-syntax-fixtures/**/*.rb", __dir__) + ].each do |file| + define_method "test_ruby_syntax_fixtures_#{file}" do + refute_nil(SyntaxTree.parse(SyntaxTree.read(file))) + end + end + end +end diff --git a/test/yarv_test.rb b/test/yarv_test.rb index 6f60d74e..e3995435 100644 --- a/test/yarv_test.rb +++ b/test/yarv_test.rb @@ -6,27 +6,27 @@ module SyntaxTree class YARVTest < Minitest::Test CASES = { - "0" => "break 0\n", - "1" => "break 1\n", - "2" => "break 2\n", - "1.0" => "break 1.0\n", - "1 + 2" => "break 1 + 2\n", - "1 - 2" => "break 1 - 2\n", - "1 * 2" => "break 1 * 2\n", - "1 / 2" => "break 1 / 2\n", - "1 % 2" => "break 1 % 2\n", - "1 < 2" => "break 1 < 2\n", - "1 <= 2" => "break 1 <= 2\n", - "1 > 2" => "break 1 > 2\n", - "1 >= 2" => "break 1 >= 2\n", - "1 == 2" => "break 1 == 2\n", - "1 != 2" => "break 1 != 2\n", - "1 & 2" => "break 1 & 2\n", - "1 | 2" => "break 1 | 2\n", - "1 << 2" => "break 1 << 2\n", - "1 >> 2" => "break 1.>>(2)\n", - "1 ** 2" => "break 1.**(2)\n", - "a = 1; a" => "a = 1\nbreak a\n" + "0" => "return 0\n", + "1" => "return 1\n", + "2" => "return 2\n", + "1.0" => "return 1.0\n", + "1 + 2" => "return 1 + 2\n", + "1 - 2" => "return 1 - 2\n", + "1 * 2" => "return 1 * 2\n", + "1 / 2" => "return 1 / 2\n", + "1 % 2" => "return 1 % 2\n", + "1 < 2" => "return 1 < 2\n", + "1 <= 2" => "return 1 <= 2\n", + "1 > 2" => "return 1 > 2\n", + "1 >= 2" => "return 1 >= 2\n", + "1 == 2" => "return 1 == 2\n", + "1 != 2" => "return 1 != 2\n", + "1 & 2" => "return 1 & 2\n", + "1 | 2" => "return 1 | 2\n", + "1 << 2" => "return 1 << 2\n", + "1 >> 2" => "return 1.>>(2)\n", + "1 ** 2" => "return 1.**(2)\n", + "a = 1; a" => "a = 1\nreturn a\n" }.freeze CASES.each do |source, expected| @@ -288,6 +288,41 @@ def value end end + instructions = + YARV.constants.map { YARV.const_get(_1) } + + YARV::Legacy.constants.map { YARV::Legacy.const_get(_1) } - + [ + YARV::Assembler, + YARV::Bf, + YARV::CallData, + YARV::Compiler, + YARV::Decompiler, + YARV::Disassembler, + YARV::InstructionSequence, + YARV::Legacy, + YARV::LocalTable, + YARV::VM + ] + + interface = %i[ + disasm + to_a + deconstruct_keys + length + pops + pushes + canonical + call + == + ] + + instructions.each do |instruction| + define_method("test_instruction_interface_#{instruction.name}") do + instance_methods = instruction.instance_methods(false) + assert_empty(interface - instance_methods) + end + end + private def assert_decompiles(expected, source)