Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lint rule against .tooltipped and introduce accessibility.yml config #2086

Merged
merged 16 commits into from
Jun 22, 2023
Merged
2 changes: 2 additions & 0 deletions .erb-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ inherit_gem:
erblint-github:
- config/accessibility.yml
linters:
NoUnusedDisable:
enabled: true
DeprecatedComponentsCounter:
enabled: true
ButtonComponentMigrationCounter:
Expand Down
7 changes: 7 additions & 0 deletions lib/primer/view_components/linters/accessibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Accessibility-focused lint rule configurations, tracked by accessibility scorecard.
camertron marked this conversation as resolved.
Show resolved Hide resolved
---
linters:
NoUnusedDisable:
enabled: true
Primer::Accessibility::TooltippedMigration:
enabled: true
14 changes: 2 additions & 12 deletions lib/primer/view_components/linters/base_linter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require "primer/view_components/constants"

require_relative "tag_tree_helpers"

require_relative "helpers/rule_helpers"
# :nocov:

module ERBLint
Expand All @@ -17,6 +17,7 @@ module Linters
# * `REQUIRED_ARGUMENTS` - optional - A list of HTML attributes that are required by the component.
class BaseLinter < Linter
include TagTreeHelpers
include Helpers::RuleHelpers

DUMP_FILE = ".erblint-counter-ignore.json"
DISALLOWED_CLASSES = [].freeze
Expand Down Expand Up @@ -134,10 +135,6 @@ def correct_counter(corrector, processed_source, offense)
end
end

def tags(processed_source)
processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
end

def counter_correct?(processed_source)
comment_node = nil
expected_count = 0
Expand Down Expand Up @@ -178,13 +175,6 @@ def counter_correct?(processed_source)
end
end

def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
message ||= klass::MESSAGE
klass_name = klass.name.demodulize
offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
end

def dump_data(processed_source)
return if @total_offenses.zero?

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

module ERBLint
module Linters
module Helpers
# Helpers for writing rules
module RuleHelpers
def tags(processed_source)
processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
end

def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
message ||= klass::MESSAGE
klass_name = klass.name.demodulize
offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
end

# Generate offense for ERB node tag
def generate_node_offense(klass, processed_source, node, message = nil)
message ||= klass::MESSAGE
offense = ["#{klass.name}:#{message}", node.loc.source].join("\n")
add_offense(processed_source.to_source_range(node.loc), offense)
end

def erb_nodes(processed_source)
processed_source.parser.ast.descendants(:erb)
end

def extract_ruby_from_erb_node(erb_node)
return unless erb_node.type == :erb

_, _, code_node = *erb_node
code_node.loc.source.strip
end
end
end
end
end
37 changes: 37 additions & 0 deletions lib/primer/view_components/linters/tooltipped_migration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require_relative "helpers/rule_helpers"
module ERBLint
module Linters
module Primer
module Accessibility
# Flag when `.tooltipped` is being used and offer alternatives.
class TooltippedMigration < Linter
include LinterRegistry
include Helpers::RuleHelpers

MIGRATE_TO_NEWER_TOOLTIP = ".tooltipped has been deprecated. Tooltip should never be set on a non-interactive element (e.g. <span>, <div>, <li>) " \
"Find alternatives in https://primer.style/design/guides/accessibility/tooltip-alternatives/." \
"If you're setting a tooltip on an interactive element, use the latest tooltip in https://primer.style/view-components."
TOOLTIPPED_RUBY_PATTERN = /classes:.*tooltipped/.freeze
camertron marked this conversation as resolved.
Show resolved Hide resolved

def run(processed_source)
# HTML tags
tags(processed_source).each do |tag|
next if tag.closing?

classes = tag.attributes["class"]&.value
generate_offense(self.class, processed_source, tag, MIGRATE_TO_NEWER_TOOLTIP) if classes&.include?("tooltipped")
end

# ERB nodes
erb_nodes(processed_source).each do |node|
code = extract_ruby_from_erb_node(node)
generate_node_offense(self.class, processed_source, node, MIGRATE_TO_NEWER_TOOLTIP) if code.match?(TOOLTIPPED_RUBY_PATTERN)
end
end
end
end
end
end
end
37 changes: 37 additions & 0 deletions test/lib/erblint/tooltipped_migration_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require "lib/erblint_test_case"

class TooltippedMigrationTest < ErblintTestCase
def linter_class
ERBLint::Linters::Primer::Accessibility::TooltippedMigration
end

def test_warns_if_tooltipped_class_is_used_on_html_tag
@file = "<li class='tooltipped'>List item</li>"
@linter.run(processed_source)
assert_equal 1, @linter.offenses.count
assert_match(/.tooltipped has been deprecated./, @linter.offenses.first.message)
end

def test_warns_if_tooltipped_class_is_used_on_ruby_component
@file = '<%= render SomeComponent.new(classes: "tooltipped tooltipped-multiline tooltipped-s") do %>'
@linter.run(processed_source)
assert_equal 1, @linter.offenses.count
assert_match(/.tooltipped has been deprecated./, @linter.offenses.first.message)
end

def test_does_not_warn_if_tooltipped_is_not_used
@file = "<%= render SomeComponent.new do %>"
@linter.run(processed_source)
assert_equal 0, @linter.offenses.count
end

def test_does_not_warn_if_inline_disable_comment
@file = <<~HTML
<%= render SomeComponent.new(classes: "tooltipped") do %><%# erblint:disable Primer::Accessibility::TooltippedMigration %>
camertron marked this conversation as resolved.
Show resolved Hide resolved
HTML
@linter.run_and_update_offense_status(processed_source)
assert_equal 0, @linter.offenses.count
end
end