Class: Rails::CssUnused::ViewScanner

Inherits:
Object
  • Object
show all
Defined in:
lib/rails/css_unused/view_scanner.rb

Overview

Scans Rails view templates, ViewComponent files, Phlex components, Stimulus controllers, and Ruby files for referenced CSS class names.

Handles:

ERB:       class="foo bar",  class: "foo",  class: ["foo", "bar"]
HAML:      .foo.bar,  %div.foo
Slim:      div.foo,  .foo
Ruby:      html_class: "foo",  css_classes("foo bar"),  "foo bar"
Stimulus:  this.element.classList.add("foo"),  "foo" string literals
Dynamic:   class="<%= cond ? 'foo' : 'bar' %>" — literal parts extracted
Dynamic vars: status_class = "foo-bar" — string assigned to *_class/*_classes var

Constant Summary collapse

HTML_CLASS_ATTR =

── ERB / HTML patterns ──────────────────────────────────────────────class=“foo bar baz” or class=‘foo bar’

/class\s*=\s*["']([^"'<>]+)["']/i
RUBY_CLASS_KV =

class: “foo bar” or class: ‘foo’

/class:\s*["']([^"']+)["']/
RUBY_CLASS_ARRAY =

class: [“foo”, “bar”] or class: %w[foo bar]

/class:\s*(?:\[|%w\[)\s*([^\]\n]+)/
TAG_HELPER =

tag.div(class: “foo”) content_tag(:div, class: “foo”)

/(?:content_tag|tag\.\w+)\s*[({][^)}\n]*class:\s*["']([^"']+)["']/
HAML_IMPLICIT =

── HAML patterns ───────────────────────────────────────────────────.foo, %div.foo.bar, %span.foo#id

/^[ \t]*(?:%[\w:-]+)?(\.[a-zA-Z][a-zA-Z0-9_-]*(?:\.[a-zA-Z][a-zA-Z0-9_-]*)*)/
HAML_HASH_CLASS =

Inline { class: “foo” }

/class:\s*["']([^"']+)["']/
SLIM_CLASS =

── Slim patterns ───────────────────────────────────────────────────div.foo.bar or .foo.bar on its own line

/^[ \t]*(?:[\w-]*)(\.[a-zA-Z][a-zA-Z0-9_-]*(?:\.[a-zA-Z][a-zA-Z0-9_-]*)*)/
ERB_DYNAMIC_CLASS =

── ERB dynamic interpolation ────────────────────────────────────────class=“<%= expr %>”, class=“prefix-<%= var %>” — extracts static parts

/class\s*=\s*["'][^"']*<%=[^%]+%>[^"']*["']/m
DYNAMIC_CLASS_VAR =

── Dynamic class variable detection (v0.2.1) ────────────────────────Detects string literals assigned to variables whose name ends with _class, _classes, _style, or _css — and the string contains hyphens (Ruby variable names cannot contain hyphens, so it must be a value).

Matches patterns like:

status_class = "foo-bar"
button_classes = "btn btn-primary"
["Active", "status-active"]          (array element with hyphenated string)
["Cancelled", "status-cancelled"]

Rule: any double- or single-quoted string containing at least one hyphen is unambiguously a string value (not a Ruby identifier), so we can safely extract it as a potential class name.

Pattern 1: variable ending in _class/_classes/_style/_css = “value”

/\b\w+_(?:class(?:es)?|style|css)\s*=\s*["']([^"'\n]+)["']/
HYPHENATED_STRING =

Pattern 2: any quoted string with hyphens in array/tuple context e.g. [“Active”, “status-active”] — the hyphenated strings are CSS classes

/["']([a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)+)["']/
JS_ADD_CLASS =

── Ruby / Stimulus string literals ─────────────────────────────────Any double-quoted string that looks like a space-separated class list

/(?:classList\.add|classList\.toggle|classList\.replace)\s*\(\s*["']([^"']+)["']/
JS_REMOVE_CLASS =
/(?:classList\.remove)\s*\(\s*["']([^"']+)["']/
RUBY_STRING_CLASSES =
/["']([a-zA-Z][a-zA-Z0-9_-]*(?:\s+[a-zA-Z][a-zA-Z0-9_-]*)*)[\"']/

Instance Method Summary collapse

Constructor Details

#initialize(root:, config: CssUnused.configuration) ⇒ ViewScanner

Returns a new instance of ViewScanner.



69
70
71
72
# File 'lib/rails/css_unused/view_scanner.rb', line 69

def initialize(root:, config: CssUnused.configuration)
  @root   = Pathname(root)
  @config = config
end

Instance Method Details

#used_classesObject

Returns a Set of class name strings referenced across all view files.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rails/css_unused/view_scanner.rb', line 75

def used_classes
  classes     = Set.new
  ignore_set  = @config.ignore_classes.map(&:to_s).to_set
  ignore_pats = Array(@config.ignore_patterns)

  each_view_file do |path, content|
    extract_from(content, path).each do |cls|
      next if ignore_set.include?(cls)
      next if ignore_pats.any? { |p| cls.match?(p) }
      classes << cls
    end
  end

  if @config.scan_javascript_for_classes
    each_js_file do |path, content|
      extract_js_classes(content).each { |cls| classes << cls }
    end
  end

  classes
end