erblint-github 0.0.4 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0131492978be048178906ad4c49f6b1a4106423fcbe025a0305198f7529099d
4
- data.tar.gz: 2313987a98f5c675f567be062a916e0d6e063bc1eaa0996f7ebc7c926afba399
3
+ metadata.gz: 2e31129554b451397cf438dc56e52ce9f0e21b7df4cecb59f92324b7ca0c7e5c
4
+ data.tar.gz: a17e3f541f4975cdfafb0d046a312581fe6481ba4e113f49f7cb9ed67a09d477
5
5
  SHA512:
6
- metadata.gz: 720cc4ead1c378475a94db0680c282d344902884014fea5696653ad29939931cd5e240d11abe9c2719d4d8b6291f133f0c6b98b957616c081624166072472e8c
7
- data.tar.gz: 63d7a414967f00ceae11b1ef7c10c86b308089a58736b087dff511e47a0dd83492adda87ae9ee6505691a428b90ba01e903fcb20bd35a20c12d8af1f30823c48
6
+ metadata.gz: 2aba7d9263d58382aabef89e3688993dc7bbf230199ae8b952159bc0c3287402107562537095b7184f55fa192a7bf0366ff43875f3f85fcf73ca57cb45a9ee21
7
+ data.tar.gz: 69a2e3fb5a40d79120655ba41bb38c2120f01b941b73ce888bc7e29e57fc9c31c119770f3b719fedb9cf1a8cef4db341696eea54c4b658320e80ff540dec4e64
data/README.md CHANGED
@@ -29,22 +29,26 @@ linters:
29
29
  enabled: true
30
30
  GitHub::Accessibility::ImageHasAlt:
31
31
  enabled: true
32
- GitHub::Accessibility::NoAriaLabelMisuse:
32
+ GitHub::Accessibility::NoAriaLabelMisuseCounter:
33
33
  enabled: true
34
34
  GitHub::Accessibility::NoPositiveTabIndex:
35
35
  enabled: true
36
36
  GitHub::Accessibility::NoRedundantImageAlt:
37
37
  enabled: true
38
+ GitHub::Accessibility::NoTitleAttributeCounter:
39
+ enabled: true
38
40
  ```
39
41
 
40
42
  ## Rules
41
43
 
42
44
  - [GitHub::Accessibility::AvoidBothDisabledAndAriaDisabled](./docs/rules/accessibility/avoid-both-disabled-and-aria-disabled.md)
45
+ - [GitHub::Accessibility::AvoidGenericLinkTextCounter](./docs/rules/accessibility/avoid-generic-link-text-counter.md)
43
46
  - [GitHub::Accessibility::IframeHasTitle](./docs/rules/accessibility/iframe-has-title.md)
44
47
  - [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/image-has-alt.md)
45
- - [GitHub::Accessibility::NoAriaLabelMisuse](./docs/rules/accessibility/no-aria-label-misuse.md)
48
+ - [GitHub::Accessibility::NoAriaLabelMisuseCounter](./docs/rules/accessibility/no-aria-label-misuse-counter.md)
46
49
  - [GitHub::Accessibility::NoPositiveTabIndex](./docs/rules/accessibility/no-positive-tab-index.md)
47
50
  - [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md)
51
+ - [GitHub::Accessibility::NoTitleAttributeCounter](./docs/rules/accessibility/no-title-attribute-counter.md)
48
52
 
49
53
  ## Testing
50
54
 
@@ -24,6 +24,39 @@ module ERBLint
24
24
  end
25
25
  end
26
26
 
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
+
27
60
  def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
28
61
  message ||= klass::MESSAGE
29
62
  message += "\nLearn more at https://github.com/github/erblint-github#rules.\n"
@@ -31,6 +64,13 @@ module ERBLint
31
64
  add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
32
65
  end
33
66
 
67
+ def generate_offense_from_source_range(klass, source_range, message = nil, replacement = nil)
68
+ message ||= klass::MESSAGE
69
+ message += "\nLearn more at https://github.com/github/erblint-github#rules.\n"
70
+ offense = ["#{simple_class_name}:#{message}", source_range.source].join("\n")
71
+ add_offense(source_range, offense, replacement)
72
+ end
73
+
34
74
  def possible_attribute_values(tag, attr_name)
35
75
  value = tag.attributes[attr_name]&.value || nil
36
76
  basic_conditional_code_check(value || "") || [value].compact
@@ -0,0 +1,140 @@
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 AvoidGenericLinkTextCounter < Linter
10
+ include ERBLint::Linters::CustomHelpers
11
+ include LinterRegistry
12
+
13
+ BANNED_GENERIC_TEXT = [
14
+ "Read more",
15
+ "Learn more",
16
+ "Click here",
17
+ "More",
18
+ "Link",
19
+ "Here"
20
+ ].freeze
21
+ ARIA_LABEL_ATTRIBUTES = %w[aria-labelledby aria-label].freeze
22
+
23
+ MESSAGE = "Avoid using generic link text such as #{BANNED_GENERIC_TEXT.join(', ')} which do not make sense in isolation."
24
+
25
+ def run(processed_source)
26
+ processed_source.ast.children.each_with_index do |node, index|
27
+ next unless node.methods.include?(:type) && node.type == :text
28
+
29
+ text = node.children.join.strip
30
+
31
+ # Checks HTML tags
32
+ if banned_text?(text)
33
+ prev_node = processed_source.ast.children[index - 1]
34
+ next_node = processed_source.ast.children[index + 1]
35
+
36
+ next unless tag_type?(prev_node) && tag_type?(next_node)
37
+
38
+ text_node_tag = BetterHtml::Tree::Tag.from_node(node)
39
+ prev_node_tag = BetterHtml::Tree::Tag.from_node(prev_node)
40
+ next_node_tag = BetterHtml::Tree::Tag.from_node(next_node)
41
+
42
+ aria_label = possible_attribute_values(prev_node_tag, "aria-label")
43
+ aria_labelledby = possible_attribute_values(prev_node_tag, "aria-labelledby")
44
+
45
+ # Checks if nested between two link tags.
46
+ if link_tag?(prev_node_tag) && link_tag?(next_node_tag) && next_node_tag.closing?
47
+ # Skip because we cannot reliably check accessible name from aria-labelledby, or an aria-label that is set to a variable
48
+ # with static code analysis.
49
+ next if aria_labelledby.present? || (aria_label.present? && aria_label.join.include?("<%="))
50
+ # Skip because aria-label starts with visible text which we allow. Related to Success Criterion 2.5.3: Label in Name
51
+ next if aria_label.present? && valid_accessible_name?(aria_label.join, text)
52
+
53
+ range = prev_node_tag.loc.begin_pos...text_node_tag.loc.end_pos
54
+ source_range = processed_source.to_source_range(range)
55
+ generate_offense_from_source_range(self.class, source_range)
56
+ end
57
+ end
58
+
59
+ # Checks Rails link helpers like `link_to`
60
+ node.descendants(:erb).each do |erb_node|
61
+ _, _, code_node = *erb_node
62
+ source = code_node.loc.source
63
+ ruby_node = extract_ruby_node(source)
64
+ send_node = ruby_node&.descendants(:send)&.first
65
+ next unless send_node.methods.include?(:method_name) && send_node.method_name == :link_to
66
+
67
+ banned_text = nil
68
+
69
+ send_node.child_nodes.each do |child_node|
70
+ banned_text = child_node.children.join if child_node.methods.include?(:type) && child_node.type == :str && banned_text?(child_node.children.join)
71
+ next if banned_text.blank?
72
+ next unless child_node.methods.include?(:type) && child_node.type == :hash
73
+
74
+ child_node.descendants(:pair).each do |pair_node|
75
+ next unless pair_node.children.first.type?(:sym)
76
+
77
+ # Skips if `link_to` has `aria-labelledby` or `aria-label` which cannot be evaluated accurately with ERB lint alone.
78
+ # ERB lint removes Ruby string interpolation so the `aria-label` for "<%= link_to 'Learn more', "aria-label": "Learn #{@some_variable}" %>" will
79
+ # only be `Learn` which is unreliable so we can't do checks :(
80
+ key_value = pair_node.children.first.children.join
81
+ banned_text = nil if ARIA_LABEL_ATTRIBUTES.include?(key_value)
82
+ next unless key_value == "aria"
83
+
84
+ pair_node.children[1].descendants(:sym).each do |sym_node|
85
+ banned_text = nil if sym_node.children.join == "label" || sym_node.children.join == "labelledby"
86
+ end
87
+ end
88
+ end
89
+ if banned_text.present?
90
+ tag = BetterHtml::Tree::Tag.from_node(code_node)
91
+ generate_offense(self.class, processed_source, tag)
92
+ end
93
+ banned_text = nil
94
+ end
95
+ end
96
+ counter_correct?(processed_source)
97
+ end
98
+
99
+ def autocorrect(processed_source, offense)
100
+ return unless offense.context
101
+
102
+ lambda do |corrector|
103
+ if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
104
+ # update the counter if exists
105
+ corrector.replace(offense.source_range, offense.context)
106
+ else
107
+ # add comment with counter if none
108
+ corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
109
+ end
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def banned_text?(text)
116
+ BANNED_GENERIC_TEXT.map(&:downcase).include?(text.downcase)
117
+ end
118
+
119
+ def valid_accessible_name?(aria_label, text)
120
+ aria_label.downcase.include?(text.downcase)
121
+ end
122
+
123
+ def extract_ruby_node(source)
124
+ BetterHtml::TestHelper::RubyNode.parse(source)
125
+ rescue ::Parser::SyntaxError
126
+ nil
127
+ end
128
+
129
+ def link_tag?(tag_node)
130
+ tag_node.name == "a"
131
+ end
132
+
133
+ def tag_type?(node)
134
+ node.methods.include?(:type) && node.type == :tag
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -10,8 +10,8 @@ module ERBLint
10
10
  include ERBLint::Linters::CustomHelpers
11
11
  include LinterRegistry
12
12
 
13
- MESSAGE = "<iframe> with meaningful content should have a title attribute that identifies the content."\
14
- " If <iframe> has no meaningful content, hide it from assistive technology with `aria-hidden='true'`."\
13
+ MESSAGE = "`<iframe>` with meaningful content should have a title attribute that identifies the content."\
14
+ " If `<iframe>` has no meaningful content, hide it from assistive technology with `aria-hidden='true'`."\
15
15
 
16
16
  def run(processed_source)
17
17
  tags(processed_source).each do |tag|
@@ -6,7 +6,7 @@ module ERBLint
6
6
  module Linters
7
7
  module GitHub
8
8
  module Accessibility
9
- class NoAriaLabelMisuse < Linter
9
+ class NoAriaLabelMisuseCounter < Linter
10
10
  include ERBLint::Linters::CustomHelpers
11
11
  include LinterRegistry
12
12
 
@@ -34,7 +34,21 @@ module ERBLint
34
34
  end
35
35
  end
36
36
  end
37
- rule_disabled?(processed_source)
37
+ counter_correct?(processed_source)
38
+ end
39
+
40
+ def autocorrect(processed_source, offense)
41
+ return unless offense.context
42
+
43
+ lambda do |corrector|
44
+ if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
45
+ # update the counter if exists
46
+ corrector.replace(offense.source_range, offense.context)
47
+ else
48
+ # add comment with counter if none
49
+ corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
50
+ end
51
+ end
38
52
  end
39
53
  end
40
54
  end
@@ -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
data/lib/tasks/docs.rake CHANGED
@@ -4,10 +4,9 @@ namespace :docs do
4
4
  task :coverage do
5
5
  require "erb_lint/all"
6
6
  require "erblint-github/linters"
7
-
8
- Dir[File.join(__dir__, "linters", "github/**/*.rb")].sort.each do |file|
7
+ Dir[File.join("lib", "erblint-github", "linters", "github/**/*.rb")].sort.each do |file|
9
8
  rule_documentation_path = file
10
- .gsub("#{__dir__}linters/github/", "docs/rules/")
9
+ .gsub("lib/erblint-github/linters/github/", "docs/rules/")
11
10
  .gsub(".rb", ".md")
12
11
  .tr("_", "-")
13
12
  raise "Missing rule documentation. Please document rule in #{rule_documentation_path}" unless File.file?(rule_documentation_path.to_s)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :tests do
4
+ task :coverage do
5
+ require "erb_lint/all"
6
+ require "erblint-github/linters"
7
+
8
+ Dir[File.join("lib", "erblint-github", "linters", "github/**/*.rb")].sort.each do |file|
9
+ test_path = file.gsub("lib/erblint-github/linters/github/", "test/linters/").gsub(".rb", "_test.rb")
10
+ raise "Missing test. Please add test in #{test_path}" unless File.file?(test_path.to_s)
11
+ end
12
+ puts "All rules have test coverage."
13
+ end
14
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erblint-github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-03 00:00:00.000000000 Z
11
+ date: 2022-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: erb_lint
@@ -16,84 +16,84 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.1'
19
+ version: 0.1.1
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.1'
26
+ version: 0.1.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.14'
33
+ version: '5.15'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '5.14'
40
+ version: '5.15'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: mocha
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1'
47
+ version: '1.14'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1'
54
+ version: '1.14'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '12.0'
61
+ version: 13.0.6
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '12.0'
68
+ version: 13.0.6
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 1.13.0
75
+ version: 1.30.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 1.13.0
82
+ version: 1.30.0
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rubocop-github
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.16.0
89
+ version: 0.17.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.16.0
96
+ version: 0.17.0
97
97
  description: Template style checking for GitHub Ruby repositories
98
98
  email:
99
99
  - opensource+erblint-github@github.com
@@ -106,16 +106,20 @@ files:
106
106
  - lib/erblint-github/linters.rb
107
107
  - lib/erblint-github/linters/custom_helpers.rb
108
108
  - lib/erblint-github/linters/github/accessibility/avoid_both_disabled_and_aria_disabled.rb
109
+ - lib/erblint-github/linters/github/accessibility/avoid_generic_link_text_counter.rb
109
110
  - lib/erblint-github/linters/github/accessibility/iframe_has_title.rb
110
111
  - lib/erblint-github/linters/github/accessibility/image_has_alt.rb
111
- - lib/erblint-github/linters/github/accessibility/no_aria_label_misuse.rb
112
+ - lib/erblint-github/linters/github/accessibility/no_aria_label_misuse_counter.rb
112
113
  - lib/erblint-github/linters/github/accessibility/no_positive_tab_index.rb
113
114
  - lib/erblint-github/linters/github/accessibility/no_redundant_image_alt.rb
115
+ - lib/erblint-github/linters/github/accessibility/no_title_attribute_counter.rb
114
116
  - lib/tasks/docs.rake
117
+ - lib/tasks/tests.rake
115
118
  homepage: https://github.com/github/erblint-github
116
119
  licenses:
117
120
  - MIT
118
- metadata: {}
121
+ metadata:
122
+ rubygems_mfa_required: 'true'
119
123
  post_install_message:
120
124
  rdoc_options: []
121
125
  require_paths:
@@ -124,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
128
  requirements:
125
129
  - - ">="
126
130
  - !ruby/object:Gem::Version
127
- version: 2.5.0
131
+ version: 2.6.0
128
132
  required_rubygems_version: !ruby/object:Gem::Requirement
129
133
  requirements:
130
134
  - - ">="