a11y-lint 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df2381933d808c2694937750b55b63897d22feb8b3726e8635504afa6bcb320b
4
- data.tar.gz: 5b04343e74f0ebb363cb2b2c065d67b9b20398d4b5bd12e86e89fe38a7bcfa8d
3
+ metadata.gz: 7cb772a75e7dc2bc7f36f7869efac31d0f9281a196ef237f57e2e2db4f3116c1
4
+ data.tar.gz: ddb88dd42e274631a6d289ab56b2c0353e305b37816b9232bd7689578d19ed87
5
5
  SHA512:
6
- metadata.gz: 4a71a89a925316ae52107958bed1cdc927d653ccc0425b81ad2bca612f694cdc63b52d9c277f24c1957976e5972ed5c810b831f150c353393014908e7e187a6d
7
- data.tar.gz: d9982d126da0a7f412d8d43b68e90b27fcde739a3b08c2b6cc0e948241eb18b95e9e42542e5bb7ce75048f389fec11351901da0e900076369c3889d1e8c16b0a
6
+ metadata.gz: 98b46acd6a78f33f546b6c2aa88e6ff40f7ffe89aaf196d3df0b355a7e2cd2a96d0fdc74c89796764953d47e40b3e89a26dc0008ae55528c3585f48a4d31493d
7
+ data.tar.gz: 2571c7dbaef8b3c37cea2a6426b29586c1ad5d3e5d120504ff20c3d4b70cabe8578cbcd50d2f8c677056aabee4520f264eae642be07e610290a4303317031c41
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-03-31
11
+
12
+ ### Added
13
+
14
+ - `LinkMissingAccessibleName` rule: detects `<a>` tags without accessible text content
15
+
10
16
  ## [0.4.0] - 2026-03-27
11
17
 
12
18
  ### Added
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module A11y
6
+ module Lint
7
+ module Rules
8
+ # Checks that link_to / external_link_to calls with empty text
9
+ # include an aria-label (WCAG 4.1.2).
10
+ class LinkMissingAccessibleName < Rule
11
+ LINK_METHODS = %w[link_to external_link_to].freeze
12
+
13
+ def check(node)
14
+ return unless link_with_empty_text_and_no_accessible_name?(node)
15
+
16
+ "link with empty text content requires an aria-label (WCAG 4.1.2)"
17
+ end
18
+
19
+ private
20
+
21
+ def link_with_empty_text_and_no_accessible_name?(node)
22
+ code = node.ruby_code
23
+ return false unless code
24
+
25
+ sexp = Ripper.sexp(code)
26
+ return false unless sexp
27
+
28
+ call = extract_link_call(sexp)
29
+ return false unless call
30
+
31
+ first_arg_empty_string?(call) && !aria_label_within?(call)
32
+ end
33
+
34
+ def extract_link_call(sexp)
35
+ return unless sexp.is_a?(Array)
36
+ return sexp if link_call?(sexp)
37
+
38
+ sexp.each do |child|
39
+ result = extract_link_call(child)
40
+ return result if result
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ def link_call?(sexp)
47
+ case sexp
48
+ in [:command, [:@ident, name, *], *] if LINK_METHODS.include?(name) then true
49
+ in [:method_add_arg, [:fcall, [:@ident, name, *]], *] if LINK_METHODS.include?(name) then true
50
+ else false
51
+ end
52
+ end
53
+
54
+ def first_arg_empty_string?(call)
55
+ args = extract_args(call)
56
+ return false unless args&.first
57
+
58
+ args.first in [:string_literal, [:string_content]]
59
+ end
60
+
61
+ def extract_args(call)
62
+ case call
63
+ in [:command, _, [:args_add_block, args, *]] then args
64
+ in [:method_add_arg, _, [:arg_paren, [:args_add_block, args, *]]] then args
65
+ else nil
66
+ end
67
+ end
68
+
69
+ def aria_label_within?(sexp)
70
+ return true if aria_hash_with_label?(sexp)
71
+ return true if aria_label_string_key?(sexp)
72
+ return false unless sexp.is_a?(Array)
73
+
74
+ sexp.any? { |child| aria_label_within?(child) }
75
+ end
76
+
77
+ def aria_hash_with_label?(sexp)
78
+ return false unless sexp.is_a?(Array) && sexp[0] == :assoc_new
79
+
80
+ key = sexp[1]
81
+ value = sexp[2]
82
+
83
+ (key in [:@label, "aria:", *]) && label_key_within?(value)
84
+ end
85
+
86
+ def label_key_within?(sexp)
87
+ return true if label_key?(sexp)
88
+ return false unless sexp.is_a?(Array)
89
+
90
+ sexp.any? { |child| label_key_within?(child) }
91
+ end
92
+
93
+ def label_key?(sexp)
94
+ sexp.is_a?(Array) && sexp[0] == :assoc_new && (sexp[1] in [:@label, "label:", *])
95
+ end
96
+
97
+ def aria_label_string_key?(sexp)
98
+ return false unless sexp.is_a?(Array) && sexp[0] == :assoc_new
99
+
100
+ sexp[1] in [:string_literal, [:string_content, [:@tstring_content, "aria-label", *]]]
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module A11y
4
4
  module Lint
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
data/lib/a11y/lint.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "lint/erb_node"
9
9
  require_relative "lint/rule"
10
10
  require_relative "lint/rules/image_tag_missing_alt"
11
11
  require_relative "lint/rules/img_missing_alt"
12
+ require_relative "lint/rules/link_missing_accessible_name"
12
13
  require_relative "lint/slim_runner"
13
14
  require_relative "lint/erb_runner"
14
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: a11y-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdullah Hashim
@@ -63,6 +63,7 @@ files:
63
63
  - lib/a11y/lint/rule.rb
64
64
  - lib/a11y/lint/rules/image_tag_missing_alt.rb
65
65
  - lib/a11y/lint/rules/img_missing_alt.rb
66
+ - lib/a11y/lint/rules/link_missing_accessible_name.rb
66
67
  - lib/a11y/lint/slim_runner.rb
67
68
  - lib/a11y/lint/version.rb
68
69
  - sig/a11y/lint.rbs