# frozen_string_literal: true if RUBY_ENGINE == 'opal' require 'corelib/string/unpack' end require 'set' require 'opal/parser' require 'opal/fragment' require 'opal/nodes' require 'opal/eof_content' require 'opal/errors' require 'opal/magic_comments' require 'opal/nodes/closure' require 'opal/source_map' module Opal # Compile a string of ruby code into javascript. # # @example # # Opal.compile "ruby_code" # # => "string of javascript code" # # @see Opal::Compiler.new for compiler options # # @param source [String] ruby source # @param options [Hash] compiler options # @return [String] javascript code # def self.compile(source, options = {}) Compiler.new(source, options).compile end # {Opal::Compiler} is the main class used to compile ruby to javascript code. # This class uses {Opal::Parser} to gather the sexp syntax tree for the ruby # code, and then uses {Opal::Node} to step through the sexp to generate valid # javascript. # # @example # Opal::Compiler.new("ruby code").compile # # => "javascript code" # # @example Accessing result # compiler = Opal::Compiler.new("ruby_code") # compiler.compile # compiler.result # => "javascript code" # # @example Source Maps # compiler = Opal::Compiler.new("") # compiler.compile # compiler.source_map # => # # class Compiler include Nodes::Closure::CompilerSupport # Generated code gets indented with two spaces on each scope INDENT = ' ' # All compare method nodes - used to optimize performance of # math comparisons COMPARE = %w[< > <= >=].freeze def self.module_name(path) path = File.join(File.dirname(path), File.basename(path).split('.').first) Pathname(path).cleanpath.to_s end # Defines a compiler option. # @option as: [Symbol] uses a different method name, e.g. with a question mark for booleans # @option default: [Object] the default value for the option # @option magic_comment: [Bool] allows magic-comments to override the option value def self.compiler_option(name, config = {}) method_name = config.fetch(:as, name) define_method(method_name) { option_value(name, config) } end # Fetches and memoizes the value for an option. def option_value(name, config) return @option_values[name] if @option_values.key? name default_value = config[:default] valid_values = config[:valid_values] magic_comment = config[:magic_comment] value = @options.fetch(name, default_value) if magic_comment && @magic_comments.key?(name) value = @magic_comments.fetch(name) end if valid_values && !valid_values.include?(value) raise( ArgumentError, "invalid value #{value.inspect} for option #{name.inspect} " \ "(valid values: #{valid_values.inspect})" ) end @option_values[name] = value end # @!method file # # The filename to use for compiling this code. Used for __FILE__ directives # as well as finding relative require() # # @return [String] compiler_option :file, default: '(file)' # @!method method_missing? # # adds method stubs for all used methods in file # # @return [Boolean] compiler_option :method_missing, default: true, as: :method_missing? # @!method arity_check? # # adds an arity check to every method definition # # @return [Boolean] compiler_option :arity_check, default: false, as: :arity_check? # @deprecated # @!method freezing? # # stubs out #freeze and #frozen? # # @return [Boolean] compiler_option :freezing, default: true, as: :freezing? # @!method irb? # # compile top level local vars with support for irb style vars compiler_option :irb, default: false, as: :irb? # @!method dynamic_require_severity # # how to handle dynamic requires (:error, :warning, :ignore) compiler_option :dynamic_require_severity, default: :ignore, valid_values: %i[error warning ignore] # @!method requirable? # # Prepare the code for future requires compiler_option :requirable, default: false, as: :requirable? # @!method load? # # Instantly load a requirable module compiler_option :load, default: false, as: :load? # @!method esm? # # Encourage ESM semantics, eg. exporting run result compiler_option :esm, default: false, as: :esm? # @!method no_export? # # Don't export this compile, even if ESM mode is enabled. We use # this internally in CLI, so that even if ESM output is desired, # we would only have one default export. compiler_option :no_export, default: false, as: :no_export? # @!method inline_operators? # # are operators compiled inline compiler_option :inline_operators, default: true, as: :inline_operators? compiler_option :eval, default: false, as: :eval? # @!method enable_source_location? # # Adds source_location for every method definition compiler_option :enable_source_location, default: false, as: :enable_source_location? # @!method enable_file_source_embed? # # Embeds source code along compiled files compiler_option :enable_file_source_embed, default: false, as: :enable_file_source_embed? # @!method use_strict? # # Enables JavaScript's strict mode (i.e., adds 'use strict'; statement) compiler_option :use_strict, default: false, as: :use_strict?, magic_comment: true # @!method parse_comments? # # Adds comments for every method definition compiler_option :parse_comments, default: false, as: :parse_comments? # @!method backtick_javascript? # # Allows use of a backtick operator (and `%x{}``) to embed verbatim JavaScript. # If false, backtick operator will compiler_option :backtick_javascript, default: nil, as: :backtick_javascript?, magic_comment: true # Warn about impending compatibility break def backtick_javascript_or_warn? case backtick_javascript? when true true when nil @backtick_javascript_warned ||= begin warning 'Backtick operator usage interpreted as intent to embed JavaScript; this code will ' \ 'break in Opal 2.0; add a magic comment: `# backtick_javascript: true`' true end true when false false end end compiler_option :scope_variables, default: [] # @!method async_await # # Enable async/await support and optionally enable auto-await. # # Use either true, false, an Array of Symbols, a String containing names # to auto-await separated by a comma or a Regexp. # # Auto-await awaits provided methods by default as if .__await__ was added to # them automatically. # # By default, the support is disabled (set to false). # # If the config value is not set to false, any calls to #__await__ will be # translated to ES8 await keyword which makes the scope return a Promise # and a containing scope will be async (instead of a value, it will return # a Promise). # # If the config value is an array, or a String separated by a comma, # auto-await is also enabled. # # A member of this collection can contain a wildcard character * in which # case all methods containing a given substring will be awaited. # # It can be used as a magic comment, examples: # ``` # # await: true # # await: *await* # # await: *await*, sleep, gets compiler_option :await, default: false, as: :async_await, magic_comment: true # @return [String] The compiled ruby code attr_reader :result # @return [Array] all [Opal::Fragment] used to produce result attr_reader :fragments # Current scope attr_accessor :scope # Top scope attr_accessor :top_scope # Current case_stmt attr_reader :case_stmt # Any content in __END__ special construct attr_reader :eof_content # Comments from the source code attr_reader :comments # Method calls made in this file attr_reader :method_calls # Magic comment flags extracted from the leading comments attr_reader :magic_comments # Access the source code currently processed attr_reader :source # Set if some rewritter caused a dynamic cache result, meaning it's not # fit to be cached attr_accessor :dynamic_cache_result def initialize(source, options = {}) @source = source @indent = '' @unique = 0 @options = options @comments = Hash.new([]) @case_stmt = nil @method_calls = Set.new @option_values = {} @magic_comments = {} @dynamic_cache_result = false end # Compile some ruby code to a string. # # @return [String] javascript code def compile parse @fragments = re_raise_with_location { process(@sexp).flatten } @fragments << fragment("\n", nil, s(:newline)) unless @fragments.last.code.end_with?("\n") @result = @fragments.map(&:code).join('') end def parse @buffer = ::Opal::Parser::SourceBuffer.new(file, 1) @buffer.source = @source @parser = Opal::Parser.default_parser sexp, comments, tokens = re_raise_with_location { @parser.tokenize(@buffer) } kind = case when requirable? :require when eval? :eval else :main end @sexp = sexp.tap { |i| i.meta[:kind] = kind } first_node = sexp.children.first if sexp.children.first.location @comments = ::Parser::Source::Comment.associate_locations(first_node, comments) @magic_comments = MagicComments.parse(first_node, comments) @eof_content = EofContent.new(tokens, @source).eof end # Returns a source map that can be used in the browser to map back to # original ruby code. # # @param source_file [String] optional source_file to reference ruby source # @return [Opal::SourceMap] def source_map # We only use @source_map if compiler is cached. @source_map || ::Opal::SourceMap::File.new(@fragments, file, @source, @result) end # Any helpers required by this file. Used by {Opal::Nodes::Top} to reference # runtime helpers that are needed. These are used to minify resulting # javascript by keeping a reference to helpers used. # # @return [Set] def helpers @helpers ||= Set.new( magic_comments[:helpers].to_s.split(',').map { |h| h.strip.to_sym } ) end def record_method_call(mid) @method_calls << mid end alias async_await_before_typecasting async_await def async_await if defined? @async_await @async_await else original = async_await_before_typecasting @async_await = case original when String async_await_set_to_regexp(original.split(',').map { |h| h.strip.to_sym }) when Array, Set async_await_set_to_regexp(original.to_a.map(&:to_sym)) when Regexp, true, false original else raise 'A value of await compiler option can be either ' \ 'a Set, an Array, a String or a Boolean.' end end end def async_await_set_to_regexp(set) set = set.map { |name| Regexp.escape(name.to_s).gsub('\*', '.*?') } set = set.join('|') /^(#{set})$/ end # This is called when a parsing/processing error occurs. This # method simply appends the filename and curent line number onto # the message and raises it. def error(msg, line = nil) error = ::Opal::SyntaxError.new(msg) error.location = Opal::OpalBacktraceLocation.new(file, line) raise error end def re_raise_with_location yield rescue StandardError, ::Opal::SyntaxError => error opal_location = ::Opal.opal_location_from_error(error) opal_location.path = file opal_location.label ||= @source.lines[opal_location.line.to_i - 1].strip new_error = ::Opal::SyntaxError.new(error.message) new_error.set_backtrace error.backtrace ::Opal.add_opal_location_to_error(opal_location, new_error) raise new_error end # This is called when a parsing/processing warning occurs. This # method simply appends the filename and curent line number onto # the message and issues a warning. def warning(msg, line = nil) warn "warning: #{msg} -- #{file}:#{line}" end # Instances of `Scope` can use this to determine the current # scope indent. The indent is used to keep generated code easily # readable. def parser_indent @indent end # Create a new sexp using the given parts. def s(type, *children) ::Opal::AST::Node.new(type, children) end def fragment(str, scope, sexp = nil) Fragment.new(str, scope, sexp) end # Used to generate a unique id name per file. These are used # mainly to name method bodies for methods that use blocks. def unique_temp(name) name = name.to_s if name && !name.empty? name = name .to_s .gsub('<=>', '$lt_eq_gt') .gsub('===', '$eq_eq_eq') .gsub('==', '$eq_eq') .gsub('=~', '$eq_tilde') .gsub('!~', '$excl_tilde') .gsub('!=', '$not_eq') .gsub('<=', '$lt_eq') .gsub('>=', '$gt_eq') .gsub('=', '$eq') .gsub('?', '$ques') .gsub('!', '$excl') .gsub('/', '$slash') .gsub('%', '$percent') .gsub('+', '$plus') .gsub('-', '$minus') .gsub('<', '$lt') .gsub('>', '$gt') .gsub(/[^\w\$]/, '$') end unique = (@unique += 1) "#{'$' unless name.start_with?('$')}#{name}$#{unique}" end # Use the given helper def helper(name) helpers << name end # To keep code blocks nicely indented, this will yield a block after # adding an extra layer of indent, and then returning the resulting # code after reverting the indent. def indent indent = @indent @indent += INDENT @space = "\n#{@indent}" res = yield @indent = indent @space = "\n#{@indent}" res end # Temporary varibales will be needed from time to time in the # generated code, and this method will assign (or reuse) on # while the block is yielding, and queue it back up once it is # finished. Variables are queued once finished with to save the # numbers of variables needed at runtime. def with_temp tmp = @scope.new_temp res = yield tmp @scope.queue_temp tmp res end # Used when we enter a while statement. This pushes onto the current # scope's while stack so we know how to handle break, next etc. def in_while return unless block_given? @while_loop = @scope.push_while result = yield @scope.pop_while result end def in_case return unless block_given? old = @case_stmt @case_stmt = {} yield @case_stmt = old end # Returns true if the parser is curently handling a while sexp, # false otherwise. def in_while? @scope.in_while? end # Process the given sexp by creating a node instance, based on its type, # and compiling it to fragments. def process(sexp, level = :expr) return fragment('', scope) if sexp.nil? if handler = handlers[sexp.type] return handler.new(sexp, level, self).compile_to_fragments else error "Unsupported sexp: #{sexp.type}" end end def handlers @handlers ||= Opal::Nodes::Base.handlers end # An array of requires used in this file def requires @requires ||= [] end # An array of trees required in this file # (typically by calling #require_tree) def required_trees @required_trees ||= [] end # An array of things (requires, trees) which don't need to success in # loading compile-time. def autoloads @autoloads ||= [] end # The last sexps in method bodies, for example, need to be returned # in the compiled javascript. Due to syntax differences between # javascript any ruby, some sexps need to be handled specially. For # example, `if` statemented cannot be returned in javascript, so # instead the "truthy" and "falsy" parts of the if statement both # need to be returned instead. # # Sexps that need to be returned are passed to this method, and the # alterned/new sexps are returned and should be used instead. Most # sexps can just be added into a `s(:return) sexp`, so that is the # default action if no special case is required. def returns(sexp) return returns s(:nil) unless sexp case sexp.type when :undef # undef :method_name always returns nil returns sexp.updated(:begin, [sexp, s(:nil)]) when :break, :next, :redo, :retry sexp when :yield sexp.updated(:returnable_yield, nil) when :when *when_sexp, then_sexp = *sexp sexp.updated(nil, [*when_sexp, returns(then_sexp)]) when :rescue body_sexp, *resbodies, else_sexp = *sexp resbodies = resbodies.map do |resbody| returns(resbody) end if else_sexp else_sexp = returns(else_sexp) end sexp.updated( nil, [ returns(body_sexp), *resbodies, else_sexp ] ) when :resbody klass, lvar, body = *sexp sexp.updated(nil, [klass, lvar, returns(body)]) when :ensure rescue_sexp, ensure_body = *sexp sexp = sexp.updated(nil, [returns(rescue_sexp), ensure_body]) sexp.updated(:js_return, [sexp]) when :begin, :kwbegin # Wrapping last expression with s(:js_return, ...) *rest, last = *sexp sexp.updated(nil, [*rest, returns(last)]) when :while, :until, :while_post, :until_post sexp when :return, :js_return, :returnable_yield sexp when :xstr if backtick_javascript_or_warn? sexp.updated(nil, [s(:js_return, *sexp.children)]) else sexp end when :if cond, true_body, false_body = *sexp sexp.updated( nil, [ cond, returns(true_body), returns(false_body) ] ).tap { |s| s.meta[:returning] = true } else if sexp.type == :send && sexp.children[1] == :debugger # debugger is a statement, so it doesn't return a value # and returning it is invalid. Therefore we update it # to do `debugger; return nil`. sexp.updated(:begin, [sexp, s(:js_return, s(:nil))]) else sexp.updated(:js_return, [sexp]) end end end def handle_block_given_call(sexp) @scope.uses_block! if @scope.block_name fragment("(#{@scope.block_name} !== nil)", scope, sexp) elsif (scope = @scope.find_parent_def) && scope.block_name fragment("(#{scope.block_name} !== nil)", scope, sexp) else fragment('false', scope, sexp) end end # Marshalling for cache shortpath def marshal_dump [@options, @option_values, @source_map ||= source_map.cache, @magic_comments, @result, @required_trees, @requires, @autoloads] end def marshal_load(src) @options, @option_values, @source_map, @magic_comments, @result, @required_trees, @requires, @autoloads = src end end end