rails-css_unused 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -23
- data/LICENSE.txt +1 -1
- data/lib/rails/css_unused/version.rb +1 -1
- data/lib/rails/css_unused/view_scanner.rb +75 -20
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e275861ec42e64ade413fc24f51105fcdfbc77691b3b8a421029523e96f8b59
|
|
4
|
+
data.tar.gz: 06da7233e0f634d4feeb02664b24f438537449a20ac2b28845866c60fb73514a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec24c67fc3da46c54954d210c4ae1ffef696abba69a32b06c7e4a56d75c9f72c65f32ad0dfea9044248fd8ff31f9adb847381b2d527215668bac1ebcb66c5619
|
|
7
|
+
data.tar.gz: 0023736da3656117d62196f8509b04a1b27622726e10772ccad96690e8cd5210b21305ad2034720dffabba50fa3b9756df73c4abee628b8ce23f92dc46ac1be7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.2.
|
|
3
|
+
## [0.2.1] - 2026-06-02
|
|
4
4
|
|
|
5
|
-
###
|
|
6
|
-
- **
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
5
|
+
### Added
|
|
6
|
+
- **Smart dynamic class variable detection** — the scanner now automatically
|
|
7
|
+
detects CSS class strings assigned to variables whose name ends in
|
|
8
|
+
`_class`, `_classes`, `_style`, or `_css` (e.g. `status_class = "foo-bar"`).
|
|
9
|
+
- **Hyphenated string literal detection** — any quoted string containing a
|
|
10
|
+
hyphen is now treated as a CSS class value. Since Ruby variable names
|
|
11
|
+
cannot contain hyphens, hyphenated quoted strings are unambiguously string
|
|
12
|
+
values, never identifiers. This eliminates false positives from patterns like:
|
|
13
|
+
```erb
|
|
14
|
+
<% status_class =
|
|
15
|
+
if exam.cancelled?
|
|
16
|
+
["Cancelled", "status-cancelled"]
|
|
17
|
+
elsif exam.approved?
|
|
18
|
+
["Approved", "status-approved"]
|
|
19
|
+
end %>
|
|
20
|
+
<span class="status-pill <%= status_class %>">
|
|
21
|
+
```
|
|
22
|
+
Classes like `status-cancelled`, `status-approved`, `status-requested`
|
|
23
|
+
are now correctly detected as used without needing `ignore_patterns`.
|
|
24
|
+
- Dynamic class var detection also runs in HAML, Slim, and Ruby component files.
|
|
10
25
|
|
|
11
|
-
###
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
- **`rake css_unused:report_verbose`** — shows ghost classes with source file inline
|
|
15
|
-
- **HAML & Slim support** — proper extraction of `.foo.bar` shorthand selectors
|
|
16
|
-
- **Slim template support** — `div.foo` class shorthand
|
|
17
|
-
- **Ruby component support** — scans ViewComponent `.rb` files for `class:` attributes
|
|
18
|
-
- **Stimulus / JS support** — `classList.add("foo")` calls detected as used classes
|
|
19
|
-
- **ERB dynamic class extraction** — `class="<%= cond ? 'a' : 'b' %>"` — static string parts extracted
|
|
20
|
-
- **`ignore_patterns`** — regex-based ignore list (e.g. `/\Ajs-/`, `/\Ais-/`)
|
|
21
|
-
- **`scan_javascript_for_classes`** — opt-in JS scanning for dynamically applied classes
|
|
22
|
-
- **`scan_ruby_components`** — opt-in scanning of `.rb` component files
|
|
23
|
-
- **Colour terminal output** — red/green/grey ANSI when outputting to a TTY
|
|
24
|
-
- **BEM double-underscore selectors** — `block__element--modifier` handled correctly
|
|
26
|
+
### Fixed
|
|
27
|
+
- False positives for dynamically assigned CSS classes used via ERB interpolation
|
|
28
|
+
(e.g. `<%= status_class %>`, `<%= button_css %>`).
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
- Confusing `stylesheet_scanner.rb` noise filter that used a hardcoded `%w[import media charset...]` list (replaced with proper context-aware skip logic)
|
|
30
|
+
## [0.2.0] - 2026-05-31
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
### Added
|
|
33
|
+
- HAML and Slim template scanning
|
|
34
|
+
- ViewComponent and Phlex component support
|
|
35
|
+
- Stimulus controller JS class scanning (`classList.add`, `classList.toggle`)
|
|
36
|
+
- `scan_javascript_for_classes` config option
|
|
37
|
+
- `scan_ruby_components` config option
|
|
38
|
+
- `show_source_files` config option
|
|
39
|
+
- `fail_on_unused` config option for CI pipelines
|
|
40
|
+
- BEM class name support (`block__element--modifier`)
|
|
41
|
+
- ERB dynamic class attribute extraction (static parts from `class="<%= expr %>"`)
|
|
42
|
+
- `ignore_patterns` config for regex-based exclusions
|
|
43
|
+
- Spinner output during scanning
|
|
44
|
+
|
|
45
|
+
## [0.1.0] - 2026-05-15
|
|
46
|
+
|
|
47
|
+
### Added
|
|
30
48
|
- Initial release
|
|
49
|
+
- CSS/SCSS/SASS stylesheet scanning
|
|
50
|
+
- ERB view scanning (`class="..."`, `class: "..."`, `class: [...]`)
|
|
51
|
+
- Ghost class report with counts
|
|
52
|
+
- `ignore_classes` configuration
|
|
53
|
+
- `css_unused:report` rake task
|
data/LICENSE.txt
CHANGED
|
@@ -12,6 +12,7 @@ module Rails
|
|
|
12
12
|
# Ruby: html_class: "foo", css_classes("foo bar"), "foo bar"
|
|
13
13
|
# Stimulus: this.element.classList.add("foo"), "foo" string literals
|
|
14
14
|
# Dynamic: class="<%= cond ? 'foo' : 'bar' %>" — literal parts extracted
|
|
15
|
+
# Dynamic vars: status_class = "foo-bar" — string assigned to *_class/*_classes var
|
|
15
16
|
class ViewScanner
|
|
16
17
|
# ── ERB / HTML patterns ──────────────────────────────────────────────
|
|
17
18
|
# class="foo bar baz" or class='foo bar'
|
|
@@ -37,12 +38,33 @@ module Rails
|
|
|
37
38
|
# class="<%= expr %>", class="prefix-<%= var %>" — extracts static parts
|
|
38
39
|
ERB_DYNAMIC_CLASS = /class\s*=\s*["'][^"']*<%=[^%]+%>[^"']*["']/m
|
|
39
40
|
|
|
41
|
+
# ── Dynamic class variable detection (v0.2.1) ────────────────────────
|
|
42
|
+
# Detects string literals assigned to variables whose name ends with
|
|
43
|
+
# _class, _classes, _style, or _css — and the string contains hyphens
|
|
44
|
+
# (Ruby variable names cannot contain hyphens, so it must be a value).
|
|
45
|
+
#
|
|
46
|
+
# Matches patterns like:
|
|
47
|
+
# status_class = "foo-bar"
|
|
48
|
+
# button_classes = "btn btn-primary"
|
|
49
|
+
# ["Active", "status-active"] (array element with hyphenated string)
|
|
50
|
+
# ["Cancelled", "status-cancelled"]
|
|
51
|
+
#
|
|
52
|
+
# Rule: any double- or single-quoted string containing at least one
|
|
53
|
+
# hyphen is unambiguously a string value (not a Ruby identifier), so
|
|
54
|
+
# we can safely extract it as a potential class name.
|
|
55
|
+
#
|
|
56
|
+
# Pattern 1: variable ending in _class/_classes/_style/_css = "value"
|
|
57
|
+
DYNAMIC_CLASS_VAR = /\b\w+_(?:class(?:es)?|style|css)\s*=\s*["']([^"'\n]+)["']/
|
|
58
|
+
#
|
|
59
|
+
# Pattern 2: any quoted string with hyphens in array/tuple context
|
|
60
|
+
# e.g. ["Active", "status-active"] — the hyphenated strings are CSS classes
|
|
61
|
+
HYPHENATED_STRING = /["']([a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)+)["']/
|
|
62
|
+
|
|
40
63
|
# ── Ruby / Stimulus string literals ─────────────────────────────────
|
|
41
64
|
# Any double-quoted string that looks like a space-separated class list
|
|
42
|
-
# Used when scan_javascript_for_classes or scan_ruby_components is on.
|
|
43
65
|
JS_ADD_CLASS = /(?:classList\.add|classList\.toggle|classList\.replace)\s*\(\s*["']([^"']+)["']/
|
|
44
|
-
JS_REMOVE_CLASS = /(?:classList\.remove)\s*\(\s*["']([^"']+)["']/
|
|
45
|
-
RUBY_STRING_CLASSES = /["']([a-zA-Z][a-zA-Z0-9_-]*(?:\s+[a-zA-Z][a-zA-Z0-9_-]*)*)["']/
|
|
66
|
+
JS_REMOVE_CLASS = /(?:classList\.remove)\s*\(\s*["']([^"']+)["']/
|
|
67
|
+
RUBY_STRING_CLASSES = /["']([a-zA-Z][a-zA-Z0-9_-]*(?:\s+[a-zA-Z][a-zA-Z0-9_-]*)*)[\"']/
|
|
46
68
|
|
|
47
69
|
def initialize(root:, config: CssUnused.configuration)
|
|
48
70
|
@root = Pathname(root)
|
|
@@ -104,7 +126,6 @@ module Rails
|
|
|
104
126
|
end
|
|
105
127
|
end
|
|
106
128
|
|
|
107
|
-
# Also scan Ruby component files if configured
|
|
108
129
|
return unless @config.scan_ruby_components
|
|
109
130
|
|
|
110
131
|
@config.component_paths.each do |rel|
|
|
@@ -132,22 +153,18 @@ module Rails
|
|
|
132
153
|
ext = path.extname
|
|
133
154
|
base = path.basename.to_s
|
|
134
155
|
|
|
135
|
-
# ERB / HTML (also used for .html.erb compound names)
|
|
136
156
|
if ext == ".erb" || base.end_with?(".html.erb")
|
|
137
157
|
found.merge extract_erb(content)
|
|
138
158
|
end
|
|
139
159
|
|
|
140
|
-
# HAML
|
|
141
160
|
if ext == ".haml" || base.end_with?(".html.haml")
|
|
142
161
|
found.merge extract_haml(content)
|
|
143
162
|
end
|
|
144
163
|
|
|
145
|
-
# Slim
|
|
146
164
|
if ext == ".slim" || base.end_with?(".html.slim")
|
|
147
165
|
found.merge extract_slim(content)
|
|
148
166
|
end
|
|
149
167
|
|
|
150
|
-
# Ruby files (ViewComponent .rb, Phlex, helpers)
|
|
151
168
|
if ext == ".rb"
|
|
152
169
|
found.merge extract_ruby(content)
|
|
153
170
|
end
|
|
@@ -171,11 +188,14 @@ module Rails
|
|
|
171
188
|
# ERB dynamic class attributes — extract static string parts
|
|
172
189
|
content.scan(ERB_DYNAMIC_CLASS) do
|
|
173
190
|
chunk = Regexp.last_match(0)
|
|
174
|
-
# Strip ERB tags, grab remaining quoted tokens
|
|
175
191
|
stripped = chunk.gsub(/<%.*?%>/m, " ")
|
|
176
192
|
stripped.scan(/["']([^"']+)["']/) { |m| found.merge tokenize(m[0]) }
|
|
177
193
|
end
|
|
178
194
|
|
|
195
|
+
# ── v0.2.1: Dynamic class variable detection ──────────────────────
|
|
196
|
+
# Detects: status_class = "foo-bar" or button_classes = "btn btn-primary"
|
|
197
|
+
found.merge extract_dynamic_class_vars(content)
|
|
198
|
+
|
|
179
199
|
found
|
|
180
200
|
end
|
|
181
201
|
|
|
@@ -183,7 +203,6 @@ module Rails
|
|
|
183
203
|
found = Set.new
|
|
184
204
|
|
|
185
205
|
content.scan(HAML_IMPLICIT) do |m|
|
|
186
|
-
# m[0] = ".foo.bar" — split on dots
|
|
187
206
|
m[0].split(".").reject(&:empty?).each do |cls|
|
|
188
207
|
found << cls if valid_class?(cls)
|
|
189
208
|
end
|
|
@@ -192,6 +211,9 @@ module Rails
|
|
|
192
211
|
content.scan(HAML_HASH_CLASS) { |m| found.merge tokenize(m[0]) }
|
|
193
212
|
content.scan(RUBY_CLASS_KV) { |m| found.merge tokenize(m[0]) }
|
|
194
213
|
|
|
214
|
+
# v0.2.1: detect dynamic class vars in HAML Ruby blocks too
|
|
215
|
+
found.merge extract_dynamic_class_vars(content)
|
|
216
|
+
|
|
195
217
|
found
|
|
196
218
|
end
|
|
197
219
|
|
|
@@ -206,23 +228,26 @@ module Rails
|
|
|
206
228
|
|
|
207
229
|
content.scan(RUBY_CLASS_KV) { |m| found.merge tokenize(m[0]) }
|
|
208
230
|
|
|
231
|
+
# v0.2.1: detect dynamic class vars in Slim Ruby blocks too
|
|
232
|
+
found.merge extract_dynamic_class_vars(content)
|
|
233
|
+
|
|
209
234
|
found
|
|
210
235
|
end
|
|
211
236
|
|
|
212
237
|
def extract_ruby(content)
|
|
213
238
|
found = Set.new
|
|
214
239
|
|
|
215
|
-
# ViewComponent / Phlex: render with class: "foo"
|
|
216
240
|
content.scan(RUBY_CLASS_KV) { |m| found.merge tokenize(m[0]) }
|
|
217
241
|
content.scan(RUBY_CLASS_ARRAY) { |m| found.merge tokenize(m[0].gsub(/["',]/, " ")) }
|
|
218
242
|
|
|
219
|
-
# Loose string literals that look like class name lists (safe: validated below)
|
|
220
243
|
content.scan(RUBY_STRING_CLASSES) do |m|
|
|
221
244
|
tokens = tokenize(m[0])
|
|
222
|
-
# Only trust them if every token is a plausible CSS class
|
|
223
245
|
found.merge(tokens) if tokens.all? { |t| valid_class?(t) }
|
|
224
246
|
end
|
|
225
247
|
|
|
248
|
+
# v0.2.1: detect dynamic class vars in Ruby component files
|
|
249
|
+
found.merge extract_dynamic_class_vars(content)
|
|
250
|
+
|
|
226
251
|
found
|
|
227
252
|
end
|
|
228
253
|
|
|
@@ -233,16 +258,49 @@ module Rails
|
|
|
233
258
|
found
|
|
234
259
|
end
|
|
235
260
|
|
|
261
|
+
# ── v0.2.1: Smart dynamic class variable extraction ──────────────────
|
|
262
|
+
#
|
|
263
|
+
# Scans content for two patterns:
|
|
264
|
+
#
|
|
265
|
+
# 1. Variables named *_class, *_classes, *_style, *_css assigned a string:
|
|
266
|
+
# status_class = "foo-bar" => extracts "foo-bar"
|
|
267
|
+
# button_classes = "btn btn-sm" => extracts "btn", "btn-sm"
|
|
268
|
+
#
|
|
269
|
+
# 2. Any quoted string containing a hyphen (unambiguous: Ruby variable
|
|
270
|
+
# names cannot contain hyphens, so hyphenated quoted strings MUST be
|
|
271
|
+
# string values, never variable names):
|
|
272
|
+
# ["Active", "status-active"] => extracts "status-active"
|
|
273
|
+
# ["Cancelled", "status-cancelled"] => extracts "status-cancelled"
|
|
274
|
+
#
|
|
275
|
+
# This directly solves the false-positive problem where classes like
|
|
276
|
+
# status-approved, status-cancelled, status-requested were flagged as
|
|
277
|
+
# ghost classes because they were assigned to a variable (status_class)
|
|
278
|
+
# and used via <%= status_class %> interpolation.
|
|
279
|
+
def extract_dynamic_class_vars(content)
|
|
280
|
+
found = Set.new
|
|
281
|
+
|
|
282
|
+
# Pattern 1: *_class/*_classes variable assignments
|
|
283
|
+
content.scan(DYNAMIC_CLASS_VAR) do |m|
|
|
284
|
+
tokenize(m[0]).each { |cls| found << cls if valid_class?(cls) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Pattern 2: hyphenated string literals (unambiguously CSS class values)
|
|
288
|
+
content.scan(HYPHENATED_STRING) do |m|
|
|
289
|
+
cls = m[0].strip
|
|
290
|
+
found << cls if valid_class?(cls)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
found
|
|
294
|
+
end
|
|
295
|
+
|
|
236
296
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
237
297
|
|
|
238
|
-
# Splits a raw class attribute value into individual class tokens.
|
|
239
|
-
# Handles: spaces, commas, quotes, ERB fragments.
|
|
240
298
|
def tokenize(raw)
|
|
241
299
|
return Set.new if raw.nil?
|
|
242
300
|
|
|
243
301
|
raw
|
|
244
|
-
.gsub(/["']/, " ")
|
|
245
|
-
.split(/[\s,]+/)
|
|
302
|
+
.gsub(/["']/, " ")
|
|
303
|
+
.split(/[\s,]+/)
|
|
246
304
|
.map { |t| t.strip.delete_prefix(".") }
|
|
247
305
|
.reject(&:empty?)
|
|
248
306
|
.reject { |t| t.include?("<%") || t.include?('#{') }
|
|
@@ -250,9 +308,6 @@ module Rails
|
|
|
250
308
|
.to_set
|
|
251
309
|
end
|
|
252
310
|
|
|
253
|
-
# A valid CSS class token: starts with a letter or underscore,
|
|
254
|
-
# contains only alphanumeric, hyphens, underscores.
|
|
255
|
-
# Allows BEM: block__element--modifier
|
|
256
311
|
def valid_class?(token)
|
|
257
312
|
token.match?(/\A-?[a-zA-Z_][a-zA-Z0-9_-]*\z/)
|
|
258
313
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-css_unused
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- sghani001
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|