Class: Rails::CssUnused::Report

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

Overview

Computes and renders the unused CSS class report.

Defined Under Namespace

Classes: Ghost

Constant Summary collapse

RESET =
"\e[0m"
BOLD =
"\e[1m"
RED =
"\e[31m"
GREEN =
"\e[32m"
YELLOW =
"\e[33m"
CYAN =
"\e[36m"
GRAY =
"\e[90m"

Instance Method Summary collapse

Constructor Details

#initialize(root:, output: $stdout, config: CssUnused.configuration) ⇒ Report

Returns a new instance of Report.



19
20
21
22
23
24
# File 'lib/rails/css_unused/report.rb', line 19

def initialize(root:, output: $stdout, config: CssUnused.configuration)
  @root   = Pathname(root)
  @output = output
  @config = config
  @tty    = output.respond_to?(:isatty) && output.isatty
end

Instance Method Details

#ghost_classesObject

Returns Array<Ghost> of unused classes.



27
28
29
30
31
32
33
34
35
36
# File 'lib/rails/css_unused/report.rb', line 27

def ghost_classes
  used    = Spinner.run("Scanning views & components") { ViewScanner.new(root: @root, config: @config).used_classes }
  defined = Spinner.run("Scanning stylesheets")        { StylesheetScanner.new(root: @root, config: @config).defined_classes_with_sources }

  unused_names = defined.map(&:name).to_set - used
  defined
    .select { |dc| unused_names.include?(dc.name) }
    .sort_by(&:name)
    .map { |dc| Ghost.new(class_name: dc.name, source_file: dc.source_file) }
end

Prints the full report to @output. Returns the exit code (0 = clean, 1 = ghosts found and fail_on_unused set).



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rails/css_unused/report.rb', line 40

def print_summary
  puts_color "", nil
  puts_color "rails-css_unused v#{Rails::CssUnused::VERSION}", BOLD

  # ── Scan with spinners ────────────────────────────────────────────
  used = Spinner.run("Scanning views & components") do
    ViewScanner.new(root: @root, config: @config).used_classes
  end

  defined = Spinner.run("Scanning stylesheets") do
    StylesheetScanner.new(root: @root, config: @config).defined_classes_with_sources
  end

  ghosts = Spinner.run("Comparing & computing ghost classes") do
    unused_names = defined.map(&:name).to_set - used
    defined
      .select { |dc| unused_names.include?(dc.name) }
      .sort_by(&:name)
      .map { |dc| Ghost.new(class_name: dc.name, source_file: dc.source_file) }
  end

  # ── Report ────────────────────────────────────────────────────────
  puts_color "", nil
  puts_color "Ghost Class Report", BOLD
  puts_color "=" * 44, GRAY
  puts_color "Project root              : #{@root}", nil
  puts_color "Stylesheet classes found  : #{defined.size}", CYAN
  puts_color "View classes referenced   : #{used.size}", CYAN
  puts_color "Ghost classes (unused)    : #{ghosts.size}", ghosts.empty? ? GREEN : RED
  puts_color "", nil

  if ghosts.empty?
    puts_color "✓ No ghost classes found — stylesheet is clean!", GREEN
  else
    puts_color "Ghost classes:", BOLD
    puts_color "", nil

    ghosts.each do |g|
      if @config.show_source_files && g.source_file
        relative = g.source_file.relative_path_from(@root) rescue g.source_file
        puts_color "  #{g.class_name}", RED
        puts_color "#{relative}", GRAY
      else
        puts_color "#{g.class_name}", RED
      end
    end

    puts_color "", nil
    puts_color "Tip: Add classes to ignore_classes or ignore_patterns in your initializer", YELLOW
    puts_color "     if they are used dynamically (JS, server-side conditions, third-party).", YELLOW
  end

  puts_color "", nil
  puts_color "Note: Dynamic class names (JS conditions, runtime interpolation) may still", GRAY
  puts_color "produce false positives. Enable scan_javascript_for_classes to reduce them.", GRAY
  puts_color "", nil

  (ghosts.any? && @config.fail_on_unused) ? 1 : 0
end