Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Embracing Ruby magic

Embracing Ruby magic

In this talk, we discuss static analysis, DSLs and the intersection between the two. We then get into the new Ruby LSP add-on APIs that allow gems to handle different types of DSL from a static analysis perspective.

Avatar for Vinicius Stock

Vinicius Stock

May 05, 2025
Tweet

More Decks by Vinicius Stock

Other Decks in Programming

Transcript

  1. Alexander Momchilov From C extension to pure C: Migrating RBS

    Day 2 16:20 Sub Hall Alexandre Terrasa Inline RBS comments for seamless type checking with Sorbet Day 3 14:00 Sub Hall
  2. What ’ s static analysis? A technique to predict what

    will happen in a program without executing it
  3. Tools enabled by static analysis All LSP features • De

    fi nition • Hover • Completion • Signature help • Type error diagnostics • Code action refactors
  4. Tools enabled by static analysis • Dead code detection •

    Unreachable code detection • Codebase visualizations
  5. Why not execute the code? Executing code may have side

    e ff ects Executing code is slow Editor performance constraints are tight
  6. def belongs_to(entity) class_eval( < < ~RUBY) def # { entity}

    end def # { entity}=( # { entity}) end RUBY end
  7. def create_method(var) define_method(var) do # . . . end end

    def create_hello create_method("hello") end
  8. Analyzing DSLs Value of static analysis drops signi fi cantly

    with decreases to accuracy or performance
  9. Ruby LSP We couldn’t handle all existing gem DSLs A

    way for gems to handle their own DSLs for static analysis
  10. Ruby LSP add-ons API for other gems to enhance the

    Ruby LSP’s features Aimed at providing tool and framework speci fi c features
  11. module MyGem class Addon < RubyLsp : : Addon def

    activate(global_state, queue) # Start background process # Handle settings end end end Basic structure
  12. module MyGem class Addon < RubyLsp : : Addon def

    name "MyGem" end end end Basic structure
  13. module MyGem class Addon < RubyLsp : : Addon def

    version "1.1.3" end end end Basic structure
  14. module MyGem class Addon < RubyLsp : : Addon def

    deactivate # S h utdown any background # processes or perform cleanup end end end Basic structure
  15. Listener traversal architecture AST traversal is performed by a visitor

    dispatcher Listeners can subscribe to node events to handle logic
  16. class SomeListener def initialize(response_builder, dispatcher) @response_builder = response_builder # Register

    this listener to handle method defs dispatcher.register(self, :on_def_node_enter) end # Define what happens when we find a method def def on_def_node_enter(node) @response_builder < < node end end
  17. # Top level entities: response building + execution # flow

    response_builder = [] dispatcher = Prism : : Dispatcher.new
  18. # Top level entities: response building + execution # flow

    response_builder = [] dispatcher = Prism : : Dispatcher.new SomeListener.new(response_builder, dispatcher)
  19. # Top level entities: response building + execution # flow

    response_builder = [] dispatcher = Prism : : Dispatcher.new SomeListener.new(response_builder, dispatcher) ast = Prism.parse("def foo; end").value dispatcher.visit(ast)
  20. # Top level entities: response building + execution # flow

    response_builder = [] dispatcher = Prism : : Dispatcher.new SomeListener.new(response_builder, dispatcher) ast = Prism.parse("def foo; end").value dispatcher.visit(ast) puts response_builder # = > [#<Prism : : DefNode name="foo">]
  21. class RailsEnhancement < RubyIndexer : : Enhancement # Found a

    method call def on_call_node_enter(node) end # Exiting that method call def on_call_node_leave(node) end end
  22. # Enhancement API # Register a new method to a

    specific class/module @listener.add_method(…) # Register a new class or module # Advances stack so that the context will be inside # that new namespace @listener.add_module(…) @listener.add_class(…) # Pops name stack (leaves the current namespace) @listener.pop_namespace_stack
  23. class RailsEnhancement < RubyIndexer : : Enhancement def on_call_node_enter(node) name

    = node.name return unless name = = :belongs_to first_arg = node.arguments&.arguments&.first unless first_arg.is_a?(Prism : : SymbolNode) return end end end
  24. class RailsEnhancement < RubyIndexer : : Enhancement def on_call_node_enter(node) #

    . . . assoc = first_arg.value loc = node.location # def user; end @listener.add_method(assoc, loc, []) end end
  25. class RailsEnhancement < RubyIndexer : : Enhancement def on_call_node_enter(node) #

    . . . sigs = [ Entry : : Signature.new( [Entry : : RequiredParameter.new( name: assoc )] ) ] # def user=(user); end @listener.add_method(" # { assoc}=", loc, sigs) end end
  26. class Post # Ruby LSP’s indexer now knows # this

    is equivalent to: # # def user; end # def user=(user); end belongs_to :user end
  27. module MyGem class Addon < RubyLsp : : Addon #

    Factory to register listeners def create_definition_listener( response_builder, uri, node_context, dispatcher) # Hook a new listener to the response builder # and dispatcher MyGem : : DefinitionListener.new( . . . ) end end end
  28. module MyGem class DefinitionListener def initialize(response_builder, index, node_context, dispatcher) @index

    = index @response_builder = response_builder @node_context = node_context dispatcher.register(self, :on_symbol_node_enter) end end end
  29. module MyGem class DefinitionListener def on_symbol_node_enter(node) # Is the method

    call surrouding this # this symbol a validate call? surrounding_call = @node_context.call_node unless surrounding_call.name = = :validate return end end end end
  30. module MyGem class DefinitionListener def on_symbol_node_enter(node) # . . .

    name = node.value return unless name end end end
  31. module MyGem class DefinitionListener def on_symbol_node_enter(node) # . . .

    methods = @index.resolve_method( name, @node_context.fully_qualified_name ) return unless methods end end end
  32. module MyGem class DefinitionListener def on_symbol_node_enter(node) # . . .

    methods.each do |m| range = range_from_location(m.location) @response_builder < < Location.new( uri: m.uri.to_s, range: range, ) end end end end
  33. Ruby LSP RuboCop Standard Reek Brakeman Rake Fabrication Action Policy

    Rspec Rails Rubyfmt Tapioca Shoulda Model context protocol (MCP)
  34. Static analysis in Ruby Ruby is very unique Looking at

    other gradual typing systems is not enough We need to experiment with new ideas
  35. Static analysis in Ruby # : [T] (Symbol entity) -

    > void : declares { # def $entity: - > T # def $entity=(T $entity) - > void # } def belongs_to(entity) end class Post belongs_to :user # T: User end
  36. Static analysis in Ruby # : (MethodSymbol) - > void

    def validate(method_name) end validate(:does_not_exist) Undefined method `does_not_exist` for User:Class
  37. class UsersController < ApplicationController before_action :set_user, only: [:show] def show

    @user.valid_method end private def set_user @user = User.find(params[:id]) end end `valid_method` doesn’t exist for NilClass
  38. class UsersController < ApplicationController before_action :set_user, only: [:show] # :

    () - > void : called_after set_user def show @user.valid_method end private def set_user @user = User.find(params[:id]) end end
  39. Correctness is important, but not at the expense of usefulness

    Optimize for most common cases with escape hatches Better type checking experience
  40. With some innovation in our type annotations, we might be

    able to account for Ruby’s dynamic nature Better type checking experience