Last active
June 26, 2025 00:36
-
-
Save barretts/b0ca07fc5fdaed37e3dc2eee2ed1f06b to your computer and use it in GitHub Desktop.
ruby check for undefined method calls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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