Skip to content

Commit 4f64ff0

Browse files
authored
Merge pull request #19 from github/kh-add_no_title_counter
Port over counter implementation and introduce linter against title attribute
2 parents 5f6d685 + 7591c1b commit 4f64ff0

File tree

7 files changed

+218
-1
lines changed

7 files changed

+218
-1
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ linters:
3535
enabled: true
3636
GitHub::Accessibility::NoRedundantImageAlt:
3737
enabled: true
38+
GitHub::Accessibility::NoTitleAttributeCounter:
39+
enabled: true
3840
```
3941
4042
## Rules
@@ -45,6 +47,7 @@ linters:
4547
- [GitHub::Accessibility::NoAriaLabelMisuse](./docs/rules/accessibility/no-aria-label-misuse.md)
4648
- [GitHub::Accessibility::NoPositiveTabIndex](./docs/rules/accessibility/no-positive-tab-index.md)
4749
- [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md)
50+
- [GitHub::Accessibility::NoTitleAttributeCounter](./docs/rules/accessibility/no-title-attribute-counter.md)
4851
4952
## Testing
5053
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# No title attribute counter
2+
3+
## Rule Details
4+
5+
The `title` attribute is strongly discouraged. The only exception is on an `<iframe>` element. It is hardly useful and cannot be accessed by multiple groups of users including keyboard-only users and mobile users.
6+
7+
The `title` attribute is commonly seen set on links, matching the link text. This is redundant and unnecessary so it can be simply be removed.
8+
9+
If you are considering the`title` attribute to provide supplementary description, consider whether the text in question can be persisted in the design. Alternatively, if it's important to display supplementary text that is hidden by default, consider using an **accessible** tooltip implementation that uses the `aria-labelledby` or `aria-describedby` semantics. Even so, proceed with caution: tooltips should only be used on interactive elements like links or buttons.
10+
11+
### Should I use the `title` attribute to provide an accessible name for an `<svg>`?
12+
13+
Use a `<title>` element instead of the `title` attribute, or an `aria-label`.
14+
15+
### Resources
16+
17+
- [TPGI: Using the HTML title attribute ](https://www.tpgi.com/using-the-html-title-attribute/)
18+
- [The Trials and Tribulations of the Title Attribute](https://www.24a11y.com/2017/the-trials-and-tribulations-of-the-title-attribute/)
19+
20+
### 👎 Examples of **incorrect** code for this rule:
21+
22+
```erb
23+
<a title="A home for all developers" href="github.com">GitHub</a>
24+
```
25+
26+
```erb
27+
<a href="/" title="github.com">GitHub</a>
28+
```
29+
30+
### 👍 Examples of **correct** code for this rule:
31+
32+
```erb
33+
<a href="github.com" aria-describedby="description">GitHub</a>
34+
<p id="description" class="tooltip js-tooltip">A home for all developers</p>
35+
```
36+
37+
```erb
38+
<a href="github.com">GitHub</a>
39+
```

lib/erblint-github/linters/custom_helpers.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,39 @@ def rule_disabled?(processed_source)
2424
end
2525
end
2626

27+
def counter_correct?(processed_source)
28+
comment_node = nil
29+
expected_count = 0
30+
rule_name = simple_class_name
31+
offenses_count = @offenses.length
32+
33+
processed_source.parser.ast.descendants(:erb).each do |node|
34+
indicator_node, _, code_node, = *node
35+
indicator = indicator_node&.loc&.source
36+
comment = code_node&.loc&.source&.strip
37+
38+
if indicator == "#" && comment.start_with?("erblint:counter") && comment.match(rule_name)
39+
comment_node = node
40+
expected_count = comment.match(/\s(\d+)\s?$/)[1].to_i
41+
end
42+
end
43+
44+
if offenses_count.zero?
45+
# have to adjust to get `\n` so we delete the whole line
46+
add_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)), "Unused erblint:counter comment for #{rule_name}", "") if comment_node
47+
return
48+
end
49+
50+
first_offense = @offenses[0]
51+
52+
if comment_node.nil?
53+
add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{offenses_count} %> to bypass this check.", "<%# erblint:counter #{rule_name} #{offenses_count} %>")
54+
else
55+
clear_offenses
56+
add_offense(processed_source.to_source_range(comment_node.loc), "Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{offenses_count}.", "<%# erblint:counter #{rule_name} #{offenses_count} %>") if expected_count != offenses_count
57+
end
58+
end
59+
2760
def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
2861
message ||= klass::MESSAGE
2962
message += "\nLearn more at https://github.com/github/erblint-github#rules.\n"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../custom_helpers"
4+
5+
module ERBLint
6+
module Linters
7+
module GitHub
8+
module Accessibility
9+
class NoTitleAttributeCounter < Linter
10+
include ERBLint::Linters::CustomHelpers
11+
include LinterRegistry
12+
13+
MESSAGE = "The title attribute should never be used unless for an `<iframe>` as it is inaccessible for several groups of users."
14+
15+
def run(processed_source)
16+
tags(processed_source).each do |tag|
17+
next if tag.name == "iframe"
18+
next if tag.closing?
19+
20+
title = possible_attribute_values(tag, "title")
21+
generate_offense(self.class, processed_source, tag) if title.present?
22+
end
23+
24+
counter_correct?(processed_source)
25+
end
26+
27+
def autocorrect(processed_source, offense)
28+
return unless offense.context
29+
30+
lambda do |corrector|
31+
if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
32+
# update the counter if exists
33+
corrector.replace(offense.source_range, offense.context)
34+
else
35+
# add comment with counter if none
36+
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end
43+
end
44+
end

test/custom_helpers_test.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,36 @@ def test_rule_disabled_adds_offense_if_disable_comment_is_present_but_no_offense
3838
assert_empty @linter.offenses
3939

4040
extended_linter.rule_disabled?(processed_source)
41+
assert_equal "Unused erblint:disable comment for CustomHelpersTest::FakeLinter", @linter.offenses.first.message
42+
end
4143

42-
assert_equal @linter.offenses.length, 1
44+
def test_counter_correct_does_not_add_offense_if_counter_matches_offense_count
45+
@file = <<~HTML
46+
<%# erblint:counter CustomHelpersTest::FakeLinter 1 %>
47+
HTML
48+
@linter.offenses = ["fake offense"]
49+
50+
extended_linter.counter_correct?(processed_source)
51+
assert_empty @linter.offenses
52+
end
53+
54+
def test_counter_correct_add_offense_if_counter_comment_is_unused
55+
@file = <<~HTML
56+
<%# erblint:counter CustomHelpersTest::FakeLinter 1 %>
57+
HTML
58+
59+
extended_linter.counter_correct?(processed_source)
60+
assert_equal "Unused erblint:counter comment for CustomHelpersTest::FakeLinter", @linter.offenses.first.message
61+
end
62+
63+
def test_counter_correct_add_offense_if_counter_comment_count_is_incorrect
64+
@file = <<~HTML
65+
<%# erblint:counter CustomHelpersTest::FakeLinter 2 %>
66+
HTML
67+
@linter.offenses = ["fake offense"]
68+
69+
extended_linter.counter_correct?(processed_source)
70+
assert_equal "Incorrect erblint:counter number for CustomHelpersTest::FakeLinter. Expected: 2, actual: 1.", @linter.offenses.first.message
4371
end
4472

4573
def test_generate_offense_with_message_defined_in_linter_class

test/linter_test_case.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,15 @@ def processed_source
2424
def tags
2525
processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
2626
end
27+
28+
def corrected_content
29+
@corrected_content ||= begin
30+
source = processed_source
31+
32+
@linter.run(source)
33+
corrector = ERBLint::Corrector.new(source, offenses)
34+
35+
corrector.corrected_content
36+
end
37+
end
2738
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class NoTitleAttributeCounterTest < LinterTestCase
6+
def linter_class
7+
ERBLint::Linters::GitHub::Accessibility::NoTitleAttributeCounter
8+
end
9+
10+
def test_warns_if_element_sets_title_and_has_no_counter_comment
11+
@file = "<img title='octopus'></img>"
12+
@linter.run(processed_source)
13+
14+
assert_equal(2, @linter.offenses.count)
15+
error_messages = @linter.offenses.map(&:message).sort
16+
assert_match(/If you must, add <%# erblint:counter GitHub::Accessibility::NoTitleAttributeCounter 1 %> to bypass this check./, error_messages.first)
17+
assert_match(/The title attribute should never be used unless for an `<iframe>` as it is inaccessible for several groups of users./, error_messages.last)
18+
end
19+
20+
def test_does_not_warns_if_element_sets_title_and_has_correct_counter_comment
21+
@file = <<~ERB
22+
<%# erblint:counter GitHub::Accessibility::NoTitleAttributeCounter 1 %>
23+
<a href="/" title="bad">some website</a>
24+
ERB
25+
@linter.run(processed_source)
26+
27+
assert_equal 0, @linter.offenses.count
28+
end
29+
30+
def test_does_not_warn_if_iframe_sets_title
31+
@file = "<iframe title='Introduction video'></iframe>"
32+
@linter.run(processed_source)
33+
34+
assert_empty @linter.offenses
35+
end
36+
37+
def test_does_not_autocorrect_when_ignores_are_correct
38+
@file = <<~ERB
39+
<%# erblint:counter GitHub::Accessibility::NoTitleAttributeCounter 1 %>
40+
<a href="/" title="bad">some website</a>
41+
ERB
42+
43+
assert_equal @file, corrected_content
44+
end
45+
46+
def test_does_autocorrect_when_ignores_are_not_correct
47+
@file = <<~ERB
48+
<%# erblint:counter GitHub::Accessibility::NoTitleAttributeCounter 3 %>
49+
<a href="/" title="bad">some website</a>
50+
ERB
51+
refute_equal @file, corrected_content
52+
53+
expected_content = <<~ERB
54+
<%# erblint:counter GitHub::Accessibility::NoTitleAttributeCounter 1 %>
55+
<a href="/" title="bad">some website</a>
56+
ERB
57+
assert_equal expected_content, corrected_content
58+
end
59+
end

0 commit comments

Comments
 (0)