Class: Railsmith::ArchChecks::DirectModelAccessChecker

Inherits:
Object
  • Object
show all
Defined in:
lib/railsmith/arch_checks/direct_model_access_checker.rb

Overview

Scans controller source files for direct ActiveRecord model access.

Flags lines like User.find(params) or Post.where(active: true) that bypass the service layer. The check is heuristic: it detects CamelCase class names followed by common AR query/persistence methods, excluding well-known non-model classes (Rails, Time, JSON, etc.).

Usage:

checker = Railsmith::ArchChecks::DirectModelAccessChecker.new
violations = checker.check(path: "app/controllers")

Constant Summary collapse

AR_METHODS =
%w[
  find find_by find_by! find_or_create_by find_or_initialize_by
  where order select limit offset joins includes eager_load preload
  all first last count sum average minimum maximum
  create create! update update_all destroy destroy_all delete delete_all
  exists? any? none? many? pluck ids
].freeze
NON_MODEL_CLASSES =

Well-known non-model CamelCase roots frequently seen in controllers.

%w[
  Rails I18n Time Date DateTime ActiveRecord ApplicationRecord ActiveModel
  ActionController ApplicationController ActionDispatch AbstractController
  ActionView ActionMailer ActiveJob ActiveSupport ActiveStorage
  Integer Float String Hash Array Symbol Numeric BigDecimal
  File Dir IO URI URL Net HTTP JSON YAML CSV
  Logger Thread Fiber Mutex Proc Method Class Module Object BasicObject
].freeze
AR_METHODS_PATTERN =
AR_METHODS.map { |m| Regexp.escape(m) }.join("|")
DETECT_RE =

Matches: CamelCase class name, dot, AR method, not followed by identifier chars. The negative lookahead ‘(?=[^a-zA-Z0-9_]|$)` prevents `find` matching `finder`.

/
  \b
  ([A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*)   (?# class name, possibly namespaced)
  \.
  (#{AR_METHODS_PATTERN})                          (?# AR method)
  (?=[^a-zA-Z0-9_]|$)                             (?# not followed by identifier chars)
/x

Instance Method Summary collapse

Instance Method Details

#check(path:) ⇒ Array<Violation>

Parameters:

  • path (String)

    directory to scan (recursively for *_controller.rb files)

Returns:



48
49
50
51
# File 'lib/railsmith/arch_checks/direct_model_access_checker.rb', line 48

def check(path:)
  Dir.glob(File.join(path, "**", "*_controller.rb"))
     .flat_map { |file| check_file(file) }
end

#check_file(file) ⇒ Array<Violation>

Parameters:

  • file (String)

    path to a single Ruby source file

Returns:



55
56
57
58
59
60
61
62
# File 'lib/railsmith/arch_checks/direct_model_access_checker.rb', line 55

def check_file(file)
  violations = []
  File.foreach(file).with_index(1) do |raw_line, lineno|
    line = raw_line.strip
    violations.concat(line_violations(file, lineno, line)) unless comment_line?(line)
  end
  violations
end