erblint-github 0.0.3 → 0.0.6

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: 46d00ab6d8c5bc82e5ea9e42c316037af0509e6f16a107cfd7762c21bac5c762
4
- data.tar.gz: 87f2785cf56cd632f122eb9ddb2e369298902737e281ca75de514fffa45de514
3
+ metadata.gz: 310138254b0989437e8cf803d52cba66ad005d7cae43b8b6233e638a20e40868
4
+ data.tar.gz: 654d731eb0c99317883327dd401726582d7df70995350d0006b365fe1b9670b6
5
5
  SHA512:
6
- metadata.gz: efd49409b7c581f29c45bcc884279bfcff48baefa63ec7e53e028980589b33a87fa655e20a39bb919167cb078e27a6e07ab3c7f4323eec7b02f6d258e1a7f489
7
- data.tar.gz: 24a5f6882e6795950545a47fb87c6e8950fc588976ffaa2c7135f9b363a642743f45ebc2e37ff9748360292249b5e36003c89dc8ec3adc6c77499ea12bc9336e
6
+ metadata.gz: ce1a90a1c2482b29b406787a1c530614718242de1b9fac828617c2f476db00578872bcddf52037bf8195c71beb8750916e853b5127f4de036aea5771a8edbe2b
7
+ data.tar.gz: 2a1e8844fdae5d352a6798937629e4ae70bea7d3a16c00e1a6de363dcc805044164b55e5166d9ffaf6f288bbdb135c373c2f1b1b40d27ecb0705c2cb616a833c
data/README.md CHANGED
@@ -25,20 +25,30 @@ require "erblint-github/linters"
25
25
  linters:
26
26
  GitHub::Accessibility::AvoidBothDisabledAndAriaDisabled:
27
27
  enabled: true
28
+ GitHub::Accessibility::IframeHasTitle:
29
+ enabled: true
28
30
  GitHub::Accessibility::ImageHasAlt:
29
31
  enabled: true
30
- GitHub::Accessibility::NoAriaLabelMisuse:
32
+ GitHub::Accessibility::NoAriaLabelMisuseCounter:
33
+ enabled: true
34
+ GitHub::Accessibility::NoPositiveTabIndex:
31
35
  enabled: true
32
36
  GitHub::Accessibility::NoRedundantImageAlt:
33
37
  enabled: true
38
+ GitHub::Accessibility::NoTitleAttributeCounter:
39
+ enabled: true
34
40
  ```
35
41
 
36
42
  ## Rules
37
43
 
38
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)
46
+ - [GitHub::Accessibility::IframeHasTitle](./docs/rules/accessibility/iframe-has-title.md)
39
47
  - [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/image-has-alt.md)
40
- - [GitHub::Accessibility::NoAriaLabelMisuse](./docs/rules/accessibility/no-aria-label-misuse.md)
48
+ - [GitHub::Accessibility::NoAriaLabelMisuseCounter](./docs/rules/accessibility/no-aria-label-misuse-counter.md)
49
+ - [GitHub::Accessibility::NoPositiveTabIndex](./docs/rules/accessibility/no-positive-tab-index.md)
41
50
  - [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md)
51
+ - [GitHub::Accessibility::NoTitleAttributeCounter](./docs/rules/accessibility/no-title-attribute-counter.md)
42
52
 
43
53
  ## Testing
44
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,104 @@
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
+ MESSAGE = "Avoid using generic link text such as #{BANNED_GENERIC_TEXT.join(', ')} which do not make sense in isolation."
22
+
23
+ def run(processed_source)
24
+ processed_source.ast.children.each_with_index do |node, index|
25
+ next unless node.methods.include?(:type) && node.type == :text
26
+
27
+ text = node.children.join.strip
28
+ # Checks HTML tags
29
+ if banned_text?(text)
30
+ prev_node = processed_source.ast.children[index - 1]
31
+ next_node = processed_source.ast.children[index + 1]
32
+
33
+ next unless tag_type?(prev_node) && tag_type?(next_node)
34
+
35
+ text_node_tag = BetterHtml::Tree::Tag.from_node(node)
36
+ prev_node_tag = BetterHtml::Tree::Tag.from_node(prev_node)
37
+ next_node_tag = BetterHtml::Tree::Tag.from_node(next_node)
38
+
39
+ # We only report if the text is nested between two link tags.
40
+ if link_tag?(prev_node_tag) && link_tag?(next_node_tag) && next_node_tag.closing?
41
+ range = prev_node_tag.loc.begin_pos...text_node_tag.loc.end_pos
42
+ source_range = processed_source.to_source_range(range)
43
+ generate_offense_from_source_range(self.class, source_range)
44
+ end
45
+ end
46
+
47
+ # Checks Rails link helpers like `link_to`
48
+ erb_node = node.type == :erb ? node : node.descendants(:erb).first
49
+ next unless erb_node
50
+
51
+ _, _, code_node = *erb_node
52
+ source = code_node.loc.source
53
+ ruby_node = extract_ruby_node(source)
54
+ send_node = ruby_node&.descendants(:send)&.first
55
+ next unless send_node.methods.include?(:method_name) && send_node.method_name == :link_to
56
+
57
+ send_node.child_nodes.each do |child_node|
58
+ if child_node.methods.include?(:type) && child_node.type == :str && banned_text?(child_node.children.join)
59
+ tag = BetterHtml::Tree::Tag.from_node(code_node)
60
+ generate_offense(self.class, processed_source, tag)
61
+ end
62
+ end
63
+ end
64
+ counter_correct?(processed_source)
65
+ end
66
+
67
+ def autocorrect(processed_source, offense)
68
+ return unless offense.context
69
+
70
+ lambda do |corrector|
71
+ if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
72
+ # update the counter if exists
73
+ corrector.replace(offense.source_range, offense.context)
74
+ else
75
+ # add comment with counter if none
76
+ corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def banned_text?(text)
84
+ BANNED_GENERIC_TEXT.map(&:downcase).include?(text.downcase)
85
+ end
86
+
87
+ def extract_ruby_node(source)
88
+ BetterHtml::TestHelper::RubyNode.parse(source)
89
+ rescue ::Parser::SyntaxError
90
+ nil
91
+ end
92
+
93
+ def link_tag?(tag_node)
94
+ tag_node.name == "a"
95
+ end
96
+
97
+ def tag_type?(node)
98
+ node.methods.include?(:type) && node.type == :tag
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,38 @@
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 IframeHasTitle < Linter
10
+ include ERBLint::Linters::CustomHelpers
11
+ include LinterRegistry
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'`."\
15
+
16
+ def run(processed_source)
17
+ tags(processed_source).each do |tag|
18
+ next if tag.name != "iframe"
19
+ next if tag.closing?
20
+
21
+ title = possible_attribute_values(tag, "title")
22
+
23
+ generate_offense(self.class, processed_source, tag) if title.empty? && !aria_hidden?(tag)
24
+ end
25
+
26
+ rule_disabled?(processed_source)
27
+ end
28
+
29
+ private
30
+
31
+ def aria_hidden?(tag)
32
+ tag.attributes["aria-hidden"]&.value&.present?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -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,29 @@
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 NoPositiveTabIndex < Linter
10
+ include ERBLint::Linters::CustomHelpers
11
+ include LinterRegistry
12
+
13
+ MESSAGE = "Do not use positive tabindex as it is error prone and can severely disrupt navigation experience for keyboard users"
14
+
15
+ def run(processed_source)
16
+ tags(processed_source).each do |tag|
17
+ next if tag.closing?
18
+ next unless tag.attributes["tabindex"]&.value.to_i.positive?
19
+
20
+ generate_offense(self.class, processed_source, tag)
21
+ end
22
+
23
+ rule_disabled?(processed_source)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ 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.3
4
+ version: 0.0.6
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: 2021-12-29 00:00:00.000000000 Z
11
+ date: 2022-06-15 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,14 +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
110
+ - lib/erblint-github/linters/github/accessibility/iframe_has_title.rb
109
111
  - lib/erblint-github/linters/github/accessibility/image_has_alt.rb
110
- - lib/erblint-github/linters/github/accessibility/no_aria_label_misuse.rb
112
+ - lib/erblint-github/linters/github/accessibility/no_aria_label_misuse_counter.rb
113
+ - lib/erblint-github/linters/github/accessibility/no_positive_tab_index.rb
111
114
  - lib/erblint-github/linters/github/accessibility/no_redundant_image_alt.rb
115
+ - lib/erblint-github/linters/github/accessibility/no_title_attribute_counter.rb
112
116
  - lib/tasks/docs.rake
117
+ - lib/tasks/tests.rake
113
118
  homepage: https://github.com/github/erblint-github
114
119
  licenses:
115
120
  - MIT
116
- metadata: {}
121
+ metadata:
122
+ rubygems_mfa_required: 'true'
117
123
  post_install_message:
118
124
  rdoc_options: []
119
125
  require_paths:
@@ -122,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
128
  requirements:
123
129
  - - ">="
124
130
  - !ruby/object:Gem::Version
125
- version: 2.5.0
131
+ version: 2.6.0
126
132
  required_rubygems_version: !ruby/object:Gem::Requirement
127
133
  requirements:
128
134
  - - ">="