Skip to content

Instantly share code, notes, and snippets.

@barretts
Last active June 26, 2025 00:36
Show Gist options
  • Save barretts/b0ca07fc5fdaed37e3dc2eee2ed1f06b to your computer and use it in GitHub Desktop.
Save barretts/b0ca07fc5fdaed37e3dc2eee2ed1f06b to your computer and use it in GitHub Desktop.
ruby check for undefined method calls
#!/usr/bin/env ruby
# Simple undefined method checker
# This script looks for potential undefined method calls by checking:
# 1. Method calls on objects that might not have those methods
# 2. Common patterns that could indicate undefined methods
# 3. Caching previous results to highlight new findings
require 'find'
require 'json'
require 'set'
CACHE_FILE = '.undefined_methods_cache.json'
class String
# Simple underscore method for CamelCase to snake_case
def underscore
self.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\\1_\\2').gsub(/([a-z\d])([A-Z])/,'\\1_\\2').tr("-", "_").downcase
end
end
# Ruby built-in methods
RUBY_BUILTINS = %w[
puts print p pp require include extend attr_accessor attr_reader attr_writer
raise rescue ensure begin end if unless while until for each map select reject
find find_all detect any? all? none? one? count size length empty? nil? present?
blank? to_s to_i to_f to_a to_h to_sym to_proc class super is_a? kind_of?
respond_to? method_missing send public_send instance_eval instance_exec
define_method method define_singleton_method singleton_class
freeze frozen? dup clone equal? eql? == === != <=> hash
inspect display warn exit abort system backtrace caller
sleep rand srand Time Date DateTime File Dir IO
Array Hash String Symbol Integer Float TrueClass FalseClass NilClass
Object Kernel Module Class BasicObject
]
# Rails/ActiveRecord methods
RAILS_METHODS = %w[
render redirect_to params request response session flash cookies
find find_by find_by! where create create! update update! save save!
destroy destroy! delete delete! new build valid? invalid? errors
validates validate before_action after_action around_action
before_save after_save before_create after_create before_update after_update
before_destroy after_destroy before_validation after_validation
belongs_to has_one has_many has_and_belongs_to_many
scope default_scope order limit offset group having
joins includes preload eager_load left_joins
pluck select distinct count sum average maximum minimum
exists? any? none? empty? present? blank? nil?
update_attribute update_column update_columns
touch increment decrement toggle
reload reset reset_counters
association association_ids association_ids=
build_association create_association create_association!
association_loaded? association
add_to_association remove_from_association
clear_association association.clear
association.count association.size association.length
association.empty? association.any? association.none?
association.find association.where association.create
association.build association.create! association.build!
association.delete association.destroy
association << association.push association.concat
association.delete_all association.destroy_all
association.clear association.empty?
association.reload association.reset
association.target association.loaded? association.loaded
association.proxy_association association.proxy_reflection
association.proxy_owner association.proxy_target
association.proxy_through association.proxy_foreign_key
association.proxy_primary_key association.proxy_type
association.proxy_class association.proxy_class_name
association.proxy_table_name association.proxy_foreign_key
association.proxy_primary_key association.proxy_type
association.proxy_class association.proxy_class_name
association.proxy_table_name
]
# Common Rails helper methods
RAILS_HELPERS = %w[
link_to button_to form_for form_with form_tag
text_field text_area password_field hidden_field
check_box radio_button select select_tag
label label_tag submit_tag submit
image_tag image_path asset_path javascript_include_tag stylesheet_link_tag
content_tag tag div span p h1 h2 h3 h4 h5 h6
ul ol li table tr td th thead tbody
strong em b i u code pre blockquote
br hr
escape_javascript escape_html h sanitize strip_tags
number_to_currency number_to_percentage number_to_phone
number_with_delimiter number_with_precision
distance_of_time_in_words time_ago_in_words
pluralize singularize
cycle concat capture content_for yield
javascript_tag stylesheet_tag
csrf_meta_tags csrf_meta_tag
auto_discovery_link_tag favicon_link_tag
javascript_include_tag stylesheet_link_tag
stylesheet_link_tag javascript_include_tag
favicon_link_tag auto_discovery_link_tag
csrf_meta_tags csrf_meta_tag
stylesheet_link_tag javascript_include_tag
favicon_link_tag auto_discovery_link_tag
csrf_meta_tags csrf_meta_tag
]
# Common gem methods
GEM_METHODS = %w[
# Excon/HTTP
get post put patch delete head options
# JSON
encode decode parse
# MultiJson
encode decode
# Redis
get set del exists expire ttl
# Sidekiq
perform perform_async perform_in perform_at
# Rollbar/Sentry
report error info warn debug
# I18n
t l
# JWT
encode decode
# BCrypt
hash new
# Rack
call env
# RSpec
expect describe it context before after let
# FactoryBot
create build build_stubbed attributes_for
# Capybara
visit click_on fill_in select choose check uncheck
# WebMock
stub_request allow allow_any_instance_of
# DatabaseCleaner
clean clean_with
# URI
parse encode_www_form
# CGI
escape unescape
# Base64
encode64 decode64
# Rack::Utils
build_nested_query
# Slides
log
# RequestStore
read write
# Fernet
verifier
# SalesforceIdFormatter
to_18
# Resolv::DNS
open
# YAML
load_file
]
# Combine all known methods
KNOWN_METHODS = RUBY_BUILTINS + RAILS_METHODS + RAILS_HELPERS + GEM_METHODS
def load_cache
return {} unless File.exist?(CACHE_FILE)
JSON.parse(File.read(CACHE_FILE))
rescue JSON::ParserError
{}
end
def save_cache(findings)
File.write(CACHE_FILE, JSON.pretty_generate(findings))
puts "Cache saved to #{CACHE_FILE}"
end
def format_finding(file_path, line_number, method_name, line_content)
"#{file_path}:#{line_number}: Potential undefined method: #{method_name}"
end
def collect_findings
findings = []
# Check all Ruby files in the current directory (excluding vendor and node_modules)
Find.find('.') do |path|
if File.file?(path) && path.end_with?('.rb') && !path.start_with?('./vendor/') && !path.start_with?('./node_modules/')
file_findings = check_file(path)
findings.concat(file_findings) if file_findings
end
end
findings
end
# Additional check for method calls without parentheses that might be undefined
def check_method_calls_without_parens(lines, file_path, all_defined_methods)
findings = []
lines.each_with_index do |line, index|
line_number = index + 1
# Skip comments and empty lines
next if line.strip.start_with?('#') || line.strip.empty?
# Look for method calls without parentheses that might be undefined
# This catches patterns like: return if check_for_existing_contact(profile)
if line.match?(/\b(\w+)\s*\([^)]*\)/)
method_name = line.match(/\b(\w+)\s*\(/)[1]
# Skip known methods and common patterns
next if KNOWN_METHODS.include?(method_name)
next if all_defined_methods.include?(method_name)
next if %w[if unless while until for each do end return].include?(method_name)
next if line.match?(/def\s+#{method_name}/)
next if line.match?(/attr_(reader|writer|accessor)\s+:#{method_name}/)
next if line.match?(/delegate\s+:#{method_name}/)
next if line.match?(/validates\s+:#{method_name}/)
next if line.match?(/(belongs_to|has_one|has_many|has_and_belongs_to_many)\s+:#{method_name}/)
findings << {
file: file_path,
line: line_number,
method: method_name,
content: line.strip
}
end
end
findings
end
def collect_methods_from_module_or_class(name)
# Try to find the file for the module/class in current directory (simple heuristic)
possible_files = Dir.glob("**/*#{name.underscore}.rb")
methods = []
possible_files.each do |file|
File.readlines(file).each do |line|
if (m = line.match(/^\s*def\s+([\w\?\!]+)/))
methods << m[1]
end
end
end
methods
end
def check_file(file_path)
return unless file_path.end_with?('.rb')
content = File.read(file_path)
lines = content.lines
# Collect method definitions in this file
defined_methods = lines.map { |l| l[/^\s*def\s+([\w\?\!]+)/, 1] }.compact
# Collect included/extended modules and parent class
included_modules = lines.map { |l| l[/^\s*include\s+([\w:]+)/, 1] }.compact
extended_modules = lines.map { |l| l[/^\s*extend\s+([\w:]+)/, 1] }.compact
parent_class = lines.map { |l| l[/^class\s+\w+\s*<\s*([\w:]+)/, 1] }.compact.first
# Collect methods from included/extended modules and parent class
module_methods = (included_modules + extended_modules).flat_map { |mod| collect_methods_from_module_or_class(mod) }
parent_methods = parent_class ? collect_methods_from_module_or_class(parent_class) : []
all_defined_methods = defined_methods + module_methods + parent_methods
# More specific patterns that might indicate undefined methods
patterns = [
# Method calls on params (common source of undefined methods)
/params\[:(\w+)\]\.(\w+)/,
# Method calls on request objects that might be undefined
/request\.(\w+)/,
# Method calls on user objects that might be undefined
/user\.(\w+)/,
# Method calls on model instances that might be undefined
/(\w+)\.(\w+)\s*\(/,
# Method calls without explicit receiver (could be undefined)
/^\s*(\w+)\s*\(/,
# Method calls on instance variables that might be undefined
/@(\w+)\.(\w+)/,
]
findings = []
# Check for method calls without parentheses
findings.concat(check_method_calls_without_parens(lines, file_path, all_defined_methods))
lines.each_with_index do |line, index|
line_number = index + 1
# Skip comments and empty lines
next if line.strip.start_with?('#') || line.strip.empty?
patterns.each do |pattern|
if line.match?(pattern)
match = line.match(pattern)
method_name = match[2] || match[1]
# Skip known methods
next if KNOWN_METHODS.include?(method_name)
# Skip methods defined in this file, included modules, or parent class
next if all_defined_methods.include?(method_name)
# Skip common variable names that might be mistaken for methods
next if %w[if unless while until for each do end].include?(method_name)
# Skip common Rails instance variables
next if line.match?(/@(params|request|response|session|flash|cookies|current_user|user)/)
# Skip common Rails controller methods
next if line.match?(/def\s+#{method_name}/)
# Skip method definitions
next if line.match?(/def\s+#{method_name}/)
# Skip attribute accessors
next if line.match?(/attr_(reader|writer|accessor)\s+:#{method_name}/)
# Skip delegate statements
next if line.match?(/delegate\s+:#{method_name}/)
# Skip common Rails validations
next if line.match?(/validates\s+:#{method_name}/)
# Skip common Rails associations
next if line.match?(/(belongs_to|has_one|has_many|has_and_belongs_to_many)\s+:#{method_name}/)
findings << {
file: file_path,
line: line_number,
method: method_name,
content: line.strip
}
end
end
end
findings
end
# Main execution
save_cache_flag = ARGV.include?('--save')
show_old_flag = ARGV.include?('--show-old')
if save_cache_flag
findings = collect_findings
save_cache(findings)
puts "Found #{findings.length} potential undefined methods"
else
current_findings = collect_findings
cached_findings = load_cache
# Convert cached findings to use symbols for consistency
cached_findings = cached_findings.map { |f| { file: f['file'], line: f['line'], method: f['method'], content: f['content'] } }
# Convert cached findings to a set for fast lookup
cached_keys = Set.new(cached_findings.map { |f| "#{f[:file]}:#{f[:line]}:#{f[:method]}" })
new_findings = []
seen_findings = []
total_findings = current_findings.length
current_findings.each do |finding|
key = "#{finding[:file]}:#{finding[:line]}:#{finding[:method]}"
is_new = !cached_keys.include?(key)
if is_new
new_findings << finding
else
seen_findings << finding
end
end
# Print new findings first
new_findings.each do |finding|
puts "\033[31m[NEW]\033[0m #{format_finding(finding[:file], finding[:line], finding[:method], finding[:content])}"
puts " Line: #{finding[:content]}"
puts ""
end
# Then print previously seen findings if flag is set
if show_old_flag
seen_findings.each do |finding|
puts "#{format_finding(finding[:file], finding[:line], finding[:method], finding[:content])}"
puts " Line: #{finding[:content]}"
puts ""
end
end
puts "\nSummary:"
puts " Total findings: #{total_findings}"
puts " New findings: #{new_findings.length}"
puts " Previously seen: #{seen_findings.length}"
if new_findings.length > 0
puts "\nTo save current results as cache, run: check_undefined_methods --save"
end
if !show_old_flag && seen_findings.length > 0
puts "\nTo show previously seen findings, run: check_undefined_methods --show-old"
end
end
puts "\nNote: This is a basic check that excludes common Ruby/Rails methods. Don't accept this result as truth."
#!/bin/bash
# Undefined method checker for Ruby projects
# Usage: check_undefined_methods [OPTIONS]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUBY_SCRIPT="$SCRIPT_DIR/check_undefined_methods.rb"
# Check if Ruby script exists
if [[ ! -f "$RUBY_SCRIPT" ]]; then
echo "Error: Ruby script not found at $RUBY_SCRIPT"
exit 1
fi
# Parse arguments
show_old=false
save_cache=false
while [[ $# -gt 0 ]]; do
case $1 in
--save)
save_cache=true
shift
;;
--show-old)
show_old=true
shift
;;
--help|-h)
echo "Usage: check_undefined_methods [OPTIONS]"
echo ""
echo "Options:"
echo " --save Save current findings as baseline cache"
echo " --show-old Show previously seen findings (default: only new)"
echo " --help, -h Show this help message"
echo ""
echo "Examples:"
echo " check_undefined_methods # Show only new findings"
echo " check_undefined_methods --show-old # Show all findings"
echo " check_undefined_methods --save # Save current as baseline"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Check if we're in a git repo
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: Not in a git repository"
exit 1
fi
# Check if we have Ruby files
if ! find . -name "*.rb" -not -path "./vendor/*" -not -path "./node_modules/*" | head -1 | grep -q .; then
echo "Error: No Ruby files found in current directory"
exit 1
fi
# Run the Ruby script with appropriate arguments
if [[ "$save_cache" == true ]]; then
ruby "$RUBY_SCRIPT" --save
elif [[ "$show_old" == true ]]; then
ruby "$RUBY_SCRIPT" --show-old
else
ruby "$RUBY_SCRIPT"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment