Skip to content

Instantly share code, notes, and snippets.

@skryukov
Created November 29, 2024 18:46
Show Gist options
  • Save skryukov/35539d57b51f38235faaace2c1a2c1a1 to your computer and use it in GitHub Desktop.
Save skryukov/35539d57b51f38235faaace2c1a2c1a1 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
require "active_support/inflector"
module RubyLsp
module ActionPolicy
class Addon < ::RubyLsp::Addon
def name
"ActionPolicy"
end
def activate(global_state, outgoing_queue)
end
def deactivate
end
def create_definition_listener(response_builder, node_context, uri, dispatcher)
Definition.new(response_builder, node_context, uri, dispatcher)
end
end
class Definition
include Requests::Support::Common
include ActiveSupport::Inflector
POLICY_SUPERCLASSES = ["ApplicationPolicy", "ActionPolicy::Base"].freeze
def initialize(response_builder, uri, node_context, dispatcher)
@response_builder = response_builder
@node_context = node_context
@uri = uri
@path = uri.to_standardized_path
@policy_rules_cache = {}
dispatcher.register(self, :on_symbol_node_enter)
end
def on_symbol_node_enter(node)
return unless in_authorize_call?
target = @node_context.call_node
policy_class = find_policy_class(target)
return unless policy_class
policy_path = find_policy_file(policy_class)
return unless policy_path
ensure_policy_rules_cached(policy_path)
add_definition(policy_path, node.value)
end
private
def in_authorize_call?
call = @node_context.call_node
call.is_a?(Prism::CallNode) && call.message == "authorize!"
end
def find_policy_class(target)
content = File.read(@path)
document = Prism.parse(content)
class_node = find_containing_class(document.value)
return derive_policy_from_target(target) unless class_node
class_name = class_node.constant_path.slice
return unless class_name.end_with?("Controller", "Channel")
resource_name = class_name
.delete_suffix("Controller")
.delete_suffix("Channel")
.singularize
"#{resource_name}Policy"
end
def find_containing_class(root)
return unless root.respond_to?(:statements)
root.statements.body.find do |node|
node.is_a?(Prism::ClassNode) &&
node.constant_path.slice.end_with?("Controller", "Channel")
end
end
def derive_policy_from_target(target)
return unless target.respond_to?(:slice)
target_name = case target
when Prism::InstanceVariableNode
target.slice[1..].classify
else
target.slice
end
target_name.end_with?("Policy") ? target_name : "#{target_name}Policy"
end
def find_policy_file(policy_class)
file_path = policy_class.gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase
root_path = Dir.pwd
[
File.join(root_path, "app/policies/#{file_path}.rb"),
File.join(root_path, "app/policies/#{file_path}_policy.rb"),
*Dir.glob(File.join(root_path, "app/policies/**/#{file_path}.rb")),
*Dir.glob(File.join(root_path, "app/policies/**/#{file_path}_policy.rb")),
].find { |path| File.exist?(path) }
end
def ensure_policy_rules_cached(policy_path)
return if @policy_rules_cache[policy_path]
content = File.read(policy_path)
document = Prism.parse(content)
document.value.statements.body.each do |stmt|
if stmt.is_a?(Prism::ClassNode)
@policy_rules_cache[policy_path] = extract_rules(stmt)
break
end
end
end
def extract_rules(node)
return {} unless node.body
rules = {}
private_section = false
node.body.child_nodes.each do |stmt|
case stmt
when Prism::CallNode
if stmt.message == "private"
private_section = true
elsif stmt.message == "alias_rule" && stmt.arguments&.arguments
stmt.arguments.arguments
.select { |arg| arg.is_a?(Prism::SymbolNode) }
.each { |arg| rules[arg.value] ||= stmt.location.start_line }
end
when Prism::DefNode
next if private_section
rules[stmt.name.to_s] = stmt.location.start_line
end
end
rules
end
def add_definition(policy_path, action)
rules = @policy_rules_cache[policy_path]
line_number = rules&.[](action.to_s)
return unless line_number
@response_builder << Interface::Location.new(
uri: URI::Generic.from_path(path: policy_path).to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: line_number - 1, character: 0),
end: Interface::Position.new(line: line_number - 1, character: 0),
),
)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment