Skip to content

Instantly share code, notes, and snippets.

@scotchi
Last active October 5, 2016 11:48
Show Gist options
  • Save scotchi/29ee874a4416029f23519e5af1e5f4e1 to your computer and use it in GitHub Desktop.
Save scotchi/29ee874a4416029f23519e5af1e5f4e1 to your computer and use it in GitHub Desktop.
Tool to parse out params in usage in a Rails controller and add a line to specify which strong params are allowed
#!/usr/bin/env ruby
require 'parser/current'
module Parser
module AST
class Node
def find(type, found: [], &block)
if self.type == type && (!block || block.call(self))
found.push(self)
else
children.each { |c| c.find(type, found: found, &block) if c.is_a?(self.class) }
end
found
end
def name
case type
when :class
children[0].children[1]
when :def, :sym
children[0]
when :send
children[1]
end
end
def function?(name = nil)
type == :send && (!name || self.name == name)
end
def target
type == :send && children[0]
end
def arguments
type == :send && children.drop(2)
end
end
end
end
def params_in_method(method, ignore: [])
first_children_of_or = method.find(:or).map { |c| c.children.first }
params = method.find(:send) do |f|
f.name == :[] && f.target.function?(:params)
end
required = Set.new
permitted = Set.new
params.each do |p|
next unless p.arguments.size == 1 && p.arguments.first.type == :sym
name = p.arguments.first.name
next if ignore.include?(name)
if first_children_of_or.include?(p)
permitted.add(name) unless required.include?(name)
else
required.add(name)
permitted.delete(name)
end
end
{ :required => required.to_a, :permitted => permitted.to_a }
end
def method_has_params_statement?(method)
method.find(:send) do |f|
f.target&.name == :params && [ :require, :permit ].include?(f.name)
end.empty?
end
def params_statement(params)
required = params[:required]
permitted = params[:permitted]
return '' if required.empty? && permitted.empty?
statement = 'params'
statement += ".require(#{required.map(&:inspect).join(', ')})" unless required.empty?
statement += ".permit(#{permitted.map(&:inspect).join(', ')})" unless permitted.empty?
statement
end
def write_params_statements(file, statements)
code = File.read(file)
statements.each do |method, statement|
escaped = Regexp.escape(method)
code.sub!(/(([\t ]*)def\s+#{escaped}([\( ]|$).*\n)/, "\\1\\2 #{statement}\n")
end
File.write(file, code)
end
def controller_class(controller)
controller.sub(/.*\//, '').split('_').map(&:capitalize).join + 'Controller'
end
def route_params(dir)
routes = Dir.chdir(dir) do
`rake routes 2>&1`.split("\n").drop(2).map { |l| l.sub(/.*?\//, '').split(/\s+/, 2) }
end
routes.select! { |_, controller| controller.include?('#') }
params = {}
routes.each do |route, controller|
name, method = controller.split('#')
klass = controller_class(name)
params[klass] ||= {}
params[klass][method.to_sym] = route.scan(/:\w+/).map { |s| s.sub(/^:/, '').to_sym }
end
params
end
def process(file)
default_route_params = route_params(File.dirname(file))
tree = Parser::CurrentRuby.parse(File.read(file))
tree.find(:class).each do |klass|
methods = klass.find(:def).select { |m| method_has_params_statement?(m) }
statements = methods.map do |method|
ignored = default_route_params.dig(klass.name.to_s)&.dig(method.name) || []
statement = params_statement(params_in_method(method, ignore: ignored))
puts "#{klass.name}##{method.name}\n #{statement}" unless statement.empty?
[ method.name, statement ]
end.to_h.reject { |_, v| v.empty? }
write_params_statements(file, statements)
end
end
ARGV.each { |f| process(f) }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment