Skip to content

Commit

Permalink
Add an ActionList linter (#1198)
Browse files Browse the repository at this point in the history
  • Loading branch information
camertron authored Jun 27, 2022
1 parent 3c16e7b commit 6397772
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/thick-suns-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': patch
---

Add ActionList linter
1 change: 0 additions & 1 deletion docs/content/system-arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ System arguments include most HTML attributes. For example:
| `style` | `String` | Inline styles. |
| `title` | `String` | The `title` attribute. |
| `width` | `Integer` | Width. |
| `role` | `String` | Roles from the ARIA spec. [MDN docs](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles). |

## Animation

Expand Down
73 changes: 73 additions & 0 deletions lib/primer/view_components/linters/disallow_action_list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

require "pathname"
require_relative "helpers/rubocop_helpers"

module ERBLint
module Linters
# Finds usages of ActionList CSS classes.
class DisallowActionList < Linter
include ERBLint::LinterRegistry
include TagTreeHelpers

class ConfigSchema < LinterConfig
property :ignore_files, accepts: array_of?(String), default: -> { [] }
end
self.config_schema = ConfigSchema

def run(processed_source)
return if ignored?(processed_source.filename)

class_regex = /ActionList[\w-]*/
tags, * = build_tag_tree(processed_source)

tags.each do |tag|
next if tag.closing?

classes =
if (class_attrib = tag.attributes["class"])
loc = class_attrib.value_node.loc
loc.source_buffer.source[loc.begin_pos...loc.end_pos]
else
""
end

indices = [].tap do |results|
classes.scan(class_regex) do
results << Regexp.last_match.offset(0)
end
end

next if indices.empty?

indices.each do |(start_idx, end_idx)|
new_loc = class_attrib.value_node.loc.with(
begin_pos: class_attrib.value_node.loc.begin_pos + start_idx,
end_pos: class_attrib.value_node.loc.begin_pos + end_idx
)

add_offense(
new_loc,
"ActionList classes are only designed to be used by Primer View Components and " \
"should be considered private. Please reach out in the #primer-rails Slack channel."
)
end
end
end

private

def ignored?(filename)
filename = Pathname(filename)

begin
filename = filename.relative_path_from(Pathname(Dir.getwd))
rescue ArgumentError
# raised if the filename does not have Dir.getwd as a prefix
end

@config.ignore_files.any? { |pattern| filename.fnmatch?(pattern) }
end
end
end
end
3 changes: 2 additions & 1 deletion test/linter_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
class LinterTestCase < Minitest::Test
def setup
@linter = linter_class&.new(file_loader, linter_class.config_schema.new)
@filename = "file.rb"
end

private
Expand Down Expand Up @@ -38,7 +39,7 @@ def file_loader
end

def processed_source
ERBLint::ProcessedSource.new("file.rb", @file)
ERBLint::ProcessedSource.new(@filename, @file)
end

def tags
Expand Down
56 changes: 56 additions & 0 deletions test/linters/disallow_action_list_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require "linter_test_case"

class DisallowActionListTest < LinterTestCase
def test_identifies_action_list_class
@file = "<div class='ActionList <%= foo %>'>fooo</div>"
@linter.run(processed_source)

assert @linter.offenses.size == 1

offense = @linter.offenses.first
assert offense.source_range.begin_pos == 12
assert offense.source_range.end_pos == 22
end

def test_identifies_two_action_list_classes
@file = "<div class='ActionList ActionList-item'>fooo</div>"
@linter.run(processed_source)

assert @linter.offenses.size == 2

offense = @linter.offenses[0]
assert offense.source_range.begin_pos == 12
assert offense.source_range.end_pos == 22

offense = @linter.offenses[1]
assert offense.source_range.begin_pos == 23
assert offense.source_range.end_pos == 38
end

def test_identifies_action_list_class_nested
@file = "<div><div class='ActionList <%= foo %>'>fooo</div></div>"
@linter.run(processed_source)

assert @linter.offenses.size == 1

offense = @linter.offenses.first
assert offense.source_range.begin_pos == 17
assert offense.source_range.end_pos == 27
end

def test_does_not_identify_action_list_class_in_ignored_files
@file = "<div class='ActionList <%= foo %>'>fooo</div>"
@filename = "app/components/primer/component_template.html.erb"
config = linter_class.config_schema.new(ignore_files: ["app/components/primer/*.html.erb"])
@linter = linter_class.new(file_loader, config)
@linter.run(processed_source)

assert_empty @linter.offenses
end

def linter_class
ERBLint::Linters::DisallowActionList
end
end

0 comments on commit 6397772

Please sign in to comment.