Class: Guardrails::ClassItis
- Inherits:
-
Object
- Object
- Guardrails::ClassItis
- Defined in:
- lib/guardrails/class_itis.rb
Overview
Finds repeating “class soup” — the same long class list applied to the same tag in many places. The classic AI-assisted-Rails failure mode: a 6-utility ‘class=“px-4 py-2 text-sm font-medium bg-white rounded-md”` ends up copy-pasted onto 30 buttons because the assistant doesn’t know the codebase already has a ‘ButtonComponent` or `.btn-base` class.
Distinct from CrossCodebasePatterns (structural shape, ignores classes) and PartialSimilarity (whole-partial Jaccard). This audit looks at *single elements* whose class attribute is a repeated high-cardinality literal — the cleanest signal for “extract a ButtonComponent / use @apply / add a semantic class.”
Defined Under Namespace
Classes: Cluster, Occurrence
Constant Summary collapse
- DEFAULT_MIN_CLASSES =
Below this many tokens in the class list, repetition is fine —‘<button class=“primary”>` everywhere is intentional, not soup. Soup starts when 5+ classes pile up on one element with no semantic name to anchor them.
5- DEFAULT_MIN_OCCURRENCES =
Same threshold as CrossCodebasePatterns — 2 occurrences are noise, 3+ implies a real recurring pattern worth extracting.
3- DEFAULT_MAX_OCCURRENCES_SHOWN =
10- VIEW_PATTERNS =
[ "app/views/**/*.html.erb", "app/components/**/*.html.erb" ].freeze
- IMPLICIT_IGNORE_SEGMENTS =
%w[vendor node_modules tmp public log].freeze
- IMPLICIT_IGNORE_PATTERNS =
[/\A(?:\w+_)?mailer\z/].freeze
Instance Method Summary collapse
- #find_clusters ⇒ Object
-
#initialize(root:, output: $stdout, min_classes: DEFAULT_MIN_CLASSES, min_occurrences: DEFAULT_MIN_OCCURRENCES, max_occurrences_shown: DEFAULT_MAX_OCCURRENCES_SHOWN) ⇒ ClassItis
constructor
A new instance of ClassItis.
- #run ⇒ Object
Constructor Details
#initialize(root:, output: $stdout, min_classes: DEFAULT_MIN_CLASSES, min_occurrences: DEFAULT_MIN_OCCURRENCES, max_occurrences_shown: DEFAULT_MAX_OCCURRENCES_SHOWN) ⇒ ClassItis
Returns a new instance of ClassItis.
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/guardrails/class_itis.rb', line 52 def initialize(root:, output: $stdout, min_classes: DEFAULT_MIN_CLASSES, min_occurrences: DEFAULT_MIN_OCCURRENCES, max_occurrences_shown: DEFAULT_MAX_OCCURRENCES_SHOWN) @root = Pathname(root) @output = output @min_classes = min_classes @min_occurrences = min_occurrences @max_occurrences_shown = max_occurrences_shown end |
Instance Method Details
#find_clusters ⇒ Object
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 |
# File 'lib/guardrails/class_itis.rb', line 69 def find_clusters groups = Hash.new { |h, k| h[k] = [] } view_files.each do |file| content = File.read(file, encoding: Encoding::UTF_8) result = ErbParser.parse(content) relative = file.relative_path_from(@root).to_s ErbParser.each_node(result.document) do |node| next unless node.is_a?(::Herb::AST::HTMLElementNode) tag = element_tag_name(node) next if tag.nil? classes = static_class_tokens(node) next if classes.length < @min_classes line, column = ErbParser.start_position(node) key = [tag, classes] groups[key] << Occurrence.new(file: relative, line: line, column: column) end end groups .select { |_, occs| occs.length >= @min_occurrences } .map { |(tag, classes), occs| Cluster.new(tag: tag, classes: classes, occurrences: occs) } .sort_by { |c| [-c.count, -c.class_count] } end |
#run ⇒ Object
63 64 65 66 67 |
# File 'lib/guardrails/class_itis.rb', line 63 def run clusters = find_clusters print_report(clusters) clusters end |