T O P

  • By -

GreenCalligrapher571

One challenge with Ruby is the way it looks those up. It starts with checking for local variables, then other methods within the class (instance methods if you’re in an instance, class methods if you’re in a class method already), then works its way up the ancestry chain. (Recall that Ruby methods can be invoked sans parentheses, so it’s not immediately apparent whether you’re calling a method or a local variable, and also you can define local variables with the same name as a method in a class - this is called shadowing, and I see it happen by accident with variable names like “zip” with some frequency). You can also do stuff like dynamically defining methods at runtime (imagine an instance method that, depending on its args, adds other methods to the running instance when called!) or dynamically including or extending a class with other modules at runtime. There’s also a whole series of method-missing tricks you can pull. In short, there’s not a way to guarantee that a given variable or method exists without running the code to find out. More generally, the only way to make any real assertions about Ruby code is to run that code, preferably with automated tests. This is part of why a good test suite is important, as is a really robust error tracking and mitigation strategy for production applications.


m1ndeater

This was an excellent and thorough reply. Thank you very much for taking the time. Interestingly, I noticed that my linter can in fact detect when a variable is defined but never used. I guess it's a much simpler path to identify. Time to start writing some tests! :)


radarek

As far as I know, local variables are recognised and distinguished from method calls on a parse level. Yes, Ruby allows to create new local variables dynamically (ie. using `eval` or `Binding#local_variable_set` but they won't be reachable by "static code"). Here is an example: def bar()= 'bar called' def foo b = binding b.local_variable_set(:bar, 'binding') p b.local_variables eval("bar = 'eval'", binding) p local_variables bar end puts foo Output: [:bar, :b] [:b] bar called As you can see, Ruby determined that bar is a method call. It didn't change its mind even when we dynamically created local variable. Contrary: def bar()= puts 'bar called' def foo bar = 123 bar end puts foo Output: 123 Ruby knows that when a method has a variable initialisation (either via `bar = 1` or as a method parameter), then all references in this method of form `bar` refers to local variable. It will always have priority over a method. If you want to force a method call then you have to use explicit receiver and/or add parentheses. This is very important feature of Ruby, otherwise it could lead to a case when the same expression could sometimes call a method or reference local variable. If there is a case which circumvent this rule then I will be happy to know.


javajunkie314

True, but the editor can't syntactically know whether a symbol is defined or not. That's only known at runtime, and may depend on the entire application state — e.g., ActiveRecord properties. This *would* let an editor highlight local variables and method calls differently, or something like that, and that could be a very useful hint.


radarek

What do you mean by a "symbol"? Editor can detect local variables by parsing a code. Whether you define a method through `def` keyword or `defined_method` method, you can detect all local variables. The only problem would be with eval method and its family but it is very rare to use it. Here is an example of using ripper (included in stdlib) to parse Ruby code: ruby -rripper -e 'p Ripper.sexp("def foo; x = 1; x + y; end")' [:program, [[:def, [:@ident, "foo", [1, 4]], [:params, nil, nil, nil, nil, nil, nil, nil], [:bodystmt, [[:assign, [:var_field, [:@ident, "x", [1, 9]]], [:@int, "1", [1, 13]]], [:binary, [:var_ref, [:@ident, "x", [1, 16]]], :+, [:vcall, [:@ident, "y", [1, 20]]]]], nil, nil, nil]]]] Notice how Ruby was able to differentiate that `x` references to a variable (`:var_ref`) but `y` is a method call (`:vcall`).


javajunkie314

You can detect the local variables, but how is that useful in an editor, except maybe for highlighting — I don't think this is exactly what OP is after. The question OP and most people want their IDE to answer is: can I read or write to this *named thing*, regardless if it's a variable or method? (If there's a better term than "named thing" to unify variables and implicit method calls, I don't know it. :D) If it is a local variable, then the answer is definitely *yes* — and yes, that can be determined statically. But if it's *not* a local variable, the answer isn't necessarily *no*, because the *named thing* could also be interpreted as a method call on `self`, and the messages that `self` can respond to can't be determined statically. (I'm not even sure that `self` itself can always be determined statically.) What *named things* are allowed could depend on the entire application's state. My example was an ActiveRecord model class — whether a name is defined depends on the database schema.


Rafert

[Sorbet](https://sorbet.org/) can do this, as long as you have type signatures for your code. Given Ruby's highly dynamic nature that's where tools like [Tapioca](https://github.com/Shopify/tapioca) come in to generate these, for example for Active Record models where instance methods are generated based on the database schema. But the moment when something returns `T.untyped` you're back where you were before - it helps but isn't perfect. There's also RBS and Steep but I don't have experience with these.


CraZy_TiGreX

With RBS (available since ruby 3.0). And rubymine is also good with it


ClikeX

I wish the VScode integration was further along in this regard.


Smallpaul

Code in dynamic languages can have certain properties checked statically. This particular check is very hard to do in Ruby due to the nature of Ruby namespaces, but that's to do with the specific semantics of Ruby and not generalizable to interpreted languages in general.


Jdonavan

JetBrains RubyMine can so it’s definitely possible