Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get the error line in a Ruby Opal code

class Test

  def initialize

  end

  def crash
    print x
  end

end

Test.new.crash

Clearly this snippet will crash at line 8. If you parse this with Opal, you will get this compiled code:

/* Generated by Opal 0.8.0.beta1 */
(function(Opal) {
  Opal.dynamic_require_severity = "error";
  var self = Opal.top, $scope = Opal, nil = Opal.nil, $breaker = Opal.breaker, $slice = Opal.slice, $klass = Opal.klass;

  Opal.add_stubs(['$print', '$x', '$crash', '$new']);
  (function($base, $super) {
    function $Test(){};
    var self = $Test = $klass($base, $super, 'Test', $Test);

    var def = self.$$proto, $scope = self.$$scope;

    def.$initialize = function() {
      var self = this;

      return nil;
    };

    return (def.$crash = function() {
      var self = this;

      return self.$print(self.$x());
    }, nil) && 'crash';
  })(self, null);
  return $scope.get('Test').$new().$crash();
})(Opal);

And of course it will throw the same error.

However, is there a way to determine the Ruby line where this error comes from?

I can see this question: Is there a way to show the Ruby line numbers in javascript generated by Opal, but I don't understand the answer: it leads me to https://github.com/opal/opal/tree/0-6-stable/examples/rack and I'm not sure what am I supposed to be looking at or doing.

When I run my javascript, I have an index.html file that loads opal.min.js and opal-parser.min.js, then finally I have my compiled Ruby-Javascript code in a <script> tag.

like image 690
Voldemort Avatar asked Sep 13 '15 16:09

Voldemort


1 Answers

Opal has source map support, to facilitate this kind of source level of debugging. I will not go into details about sourcemaps, but HTML5Rocks has a great article that covers the topic in depth.

Here is the minimal boilerplate to set that up with Opal:

Let index.rb be our source file:

class Test

  def initialize

  end

  def crash
    print x
  end

end

Test.new.crash

Since you would rather not use a lot of extraneous utilties, let us directly use the Opal API. Create a file builder.rb which will compile the file above:

require 'opal'
Opal::Processor.source_map_enabled = true
Opal.append_path "."

builder = Opal::Builder.new.build('index')

# Write the output file containing a referece to sourcemap
# which we generate below : this will help the browser locate the 
# sourcemap. Note that we are generating sourcemap for only code and not
# the entire Opal corelib.
#
File.binwrite "build.js", "#{builder.to_s}\n//# sourceMappingURL=build.js.map"
File.binwrite "build.js.map", builder.source_map.to_s

File.binwrite "opal_lib.js", Opal::Builder.build('opal_lib')

Also create an opal_lib.rb file containing only:

require 'opal'

Finally create an index.html which will allow us to run the script in browser.

<!DOCTYPE html>
<html>
  <head>
    <script src="opal_lib.js"></script>
    <script src="build.js"></script>
  </head>
  <body>
  </body>
</html>

Now to actually compile your file, run:

ruby builder.rb

This will generate compiled javascript files opal_lib.js and build.js which are referenced by our index.html file. Now just open index.html in your browser. You will get a full call-stack and source view:

Opal error stack screenshot

The line numbers of the source file are available:

Opal error stack trace


As an alternative to using the browser, you can also use Node.js for the same purpose. This requires you have Node.js and npm installed. You will also need to install npm module source-map-support

npm install source-map-support

Now you can open the node repl and enter the following:

require('source-map-support').install();
require('./opal_lib');
require('./build');

You will get a stack trace with correct source line numbers :

/home/gaurav/Workspace/opal-playground/opal_lib.js:4436
        Error.captureStackTrace(err);
              ^
NoMethodError: undefined method `x' for #<Test:0x102>
    at OpalClass.$new (/home/gaurav/Workspace/opal-playground/opal_lib.js:4436:15)
    at OpalClass.$exception (/home/gaurav/Workspace/opal-playground/opal_lib.js:4454:31)
    at $Test.$raise (/home/gaurav/Workspace/opal-playground/opal_lib.js:4204:31)
    at $Test.Opal.defn.TMP_1 (/home/gaurav/Workspace/opal-playground/opal_lib.js:3032:19)
    at $Test.method_missing_stub [as $x] (/home/gaurav/Workspace/opal-playground/opal_lib.js:886:35)
    at $Test.$crash (/home/gaurav/Workspace/opal-playground/index.rb:8:11)
    at /home/gaurav/Workspace/opal-playground/index.rb:13:10
    at Object.<anonymous> (/home/gaurav/Workspace/opal-playground/index.rb:13:10)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)

I recommend that you use bundler for gem management. Here is the Gemfile for fetching Opal master:

source 'http://production.cf.rubygems.org'

gem 'opal', github: 'opal/opal'

To compile you will have to run:

bundle install
bundle exec ruby builder.rb

Sprockets integration / rack integration that others have mentioned use the same API underneath, abstracting away the plumbing.


Update:

Since we have the correct line numbers in stack, it is fairly to programatically parse the stack and extract this line number into a variable:

require('./opal_lib');
require('source-map-support').install();
var $e = null;
try {
  require('./build');
} catch (e) {
  $e = e;
}
var lines = e.split('\n').map(function(line){ return line.match(/^.*\((\S+):(\d+):(\d+)\)/) })

var first_source_line;

for (var i = 0; i < lines.length ; i++) {
  var match = lines[i];
  if (match == null) continue;
  if (match[1].match(/index.rb$/) {
    first_source_line = match;
    break;
  }
}

var line_number;
if (first_source_line) line_number = first_source_line[2] // ==> 8

And of course you can do it ruby as well (but if you are running this in browser you will have to include source-map-support here as well):

class Test

  def initialize

  end

  def crash
    print x
  end

end

line_num = nil
begin
  Test.new.crash
rescue => e
  if line = e.backtrace.map{|line| line.match(/^.*\((\S+):(\d+):(\d+)\)/) }.compact.find{|match| match[1] =~ /index.rb$/ }
    line_num = line[2]
  end
end

puts "line_num => #{line_num}" # ==> 8
like image 153
lorefnon Avatar answered Oct 21 '22 01:10

lorefnon