a11y-lint 0.7.2 → 0.10.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 +4 -4
- data/.rubocop.yml +7 -1
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +8 -6
- data/README.md +13 -2
- data/lib/a11y/lint/block_inspection.rb +28 -0
- data/lib/a11y/lint/call_node.rb +108 -0
- data/lib/a11y/lint/cli.rb +13 -5
- data/lib/a11y/lint/erb_node.rb +27 -11
- data/lib/a11y/lint/erb_runner.rb +29 -3
- data/lib/a11y/lint/errors.rb +17 -0
- data/lib/a11y/lint/phlex_node.rb +98 -0
- data/lib/a11y/lint/phlex_runner.rb +136 -0
- data/lib/a11y/lint/phlex_tags.rb +132 -0
- data/lib/a11y/lint/ruby_code.rb +67 -0
- data/lib/a11y/lint/rule.rb +4 -0
- data/lib/a11y/lint/rules/perceivable/image_tag_missing_alt.rb +5 -69
- data/lib/a11y/lint/rules/robust/missing_accessible_name.rb +19 -104
- data/lib/a11y/lint/{node.rb → slim_node.rb} +59 -4
- data/lib/a11y/lint/slim_runner.rb +4 -1
- data/lib/a11y/lint/version.rb +1 -1
- data/lib/a11y/lint.rb +8 -8
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecfe715cda47636bb8dc87f9cf3655c641d33d2e10cfd8471be73b2dd7336a1f
|
|
4
|
+
data.tar.gz: 818bb7d22993ac9036ac148d60adce32c2ef7ee342bc741bbd8e64d893414d2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a0d354908cfc0c670db3556f0854e1e91f0486ae502ff6f93395a95923d1a6b7b456bd625ffb476ba8f7e8d3f8bea5f859ed4875e0cf172770be5e87da8517f
|
|
7
|
+
data.tar.gz: e0e5e582649edc60f0df35635af190beb49b2ab69c9da392e9ae31326918c6704962b5a5cd6d60f8e1bb8bf47474a9d4b2fdb18baf424f3013871a7d3cc17a10
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
-
TargetRubyVersion: 3.
|
|
2
|
+
TargetRubyVersion: 3.3
|
|
3
3
|
|
|
4
4
|
Style/StringLiterals:
|
|
5
5
|
EnforcedStyle: double_quotes
|
|
@@ -13,6 +13,12 @@ Layout/LineLength:
|
|
|
13
13
|
- "a11y-lint.gemspec"
|
|
14
14
|
|
|
15
15
|
Metrics/ClassLength:
|
|
16
|
+
Enabled: false
|
|
17
|
+
|
|
18
|
+
Metrics/ModuleLength:
|
|
19
|
+
Enabled: false
|
|
20
|
+
|
|
21
|
+
Metrics/AbcSize:
|
|
16
22
|
Exclude:
|
|
17
23
|
- "test/**/*"
|
|
18
24
|
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-04-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Introduce `A11y::Lint::CallNode` wrapper so rules express accessibility logic, not AST traversal
|
|
15
|
+
- Expose Prism `CallNode` from `SlimNode` and `ErbNode` via the new `RubyCode` parser
|
|
16
|
+
- Expose Prism `CallNode` from `PhlexNode` instead of converting to string
|
|
17
|
+
- Extract Phlex HTML tag constants into `PhlexTags` module
|
|
18
|
+
|
|
19
|
+
## [0.9.0] - 2026-04-15
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Breaking:** Require Ruby >= 3.3.0 (was 3.1.0). Prism ships with Ruby 3.3+ as a bundled gem
|
|
24
|
+
- **Breaking:** `slim` is now an optional dependency. Add `gem "slim"` to your Gemfile if you lint `.slim` files. A `SlimLoadError` is raised with a helpful message if the gem is missing
|
|
25
|
+
- `prism` is no longer a declared dependency (bundled with Ruby 3.3+)
|
|
26
|
+
- Replace Ripper with Prism for Ruby code parsing in `ImageTagMissingAlt` and `MissingAccessibleName` rules
|
|
27
|
+
- Custom error classes are now defined in `lib/a11y/lint/errors.rb`
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `MissingAccessibleName`: fix false positive when block contains visible text or HTML tags (e.g. `link_to do` with a `span` or plain text inside)
|
|
32
|
+
- Add `block_body_codes` and `block_has_text_children?` to all three node types (Slim, ERB, Phlex) so block content inspection works across all pipelines
|
|
33
|
+
|
|
34
|
+
## [0.8.0] - 2026-04-14
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Phlex view support: scans `.rb` files containing Phlex components
|
|
39
|
+
- Detects Phlex files by the presence of a `def view_template` method
|
|
40
|
+
- All existing rules (`ImgMissingAlt`, `ImageTagMissingAlt`, `ListInvalidChildren`, `MissingAccessibleName`) work with Phlex views
|
|
41
|
+
- CLI automatically discovers `.rb` files when scanning directories
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **Breaking:** `Node` has been renamed to `SlimNode` for consistency with `ErbNode` and `PhlexNode`
|
|
46
|
+
|
|
47
|
+
### Dependencies
|
|
48
|
+
|
|
49
|
+
- Added `prism` gem for Ruby AST parsing
|
|
50
|
+
|
|
10
51
|
## [0.7.2] - 2026-04-13
|
|
11
52
|
|
|
12
53
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
a11y-lint is a Ruby gem (v0.1.0) for accessibility linting. It uses the `A11y::Lint` module namespace. Requires Ruby >= 3.
|
|
7
|
+
a11y-lint is a Ruby gem (v0.1.0) for accessibility linting. It uses the `A11y::Lint` module namespace. Requires Ruby >= 3.3.0.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -19,19 +19,21 @@ a11y-lint is a Ruby gem (v0.1.0) for accessibility linting. It uses the `A11y::L
|
|
|
19
19
|
|
|
20
20
|
## Architecture
|
|
21
21
|
|
|
22
|
-
- **CLI:** `lib/a11y/lint/cli.rb` — command-line interface; executable at `exe/a11y-lint`; routes `.slim` files to `SlimRunner` and `.
|
|
22
|
+
- **CLI:** `lib/a11y/lint/cli.rb` — command-line interface; executable at `exe/a11y-lint`; routes `.slim` files to `SlimRunner`, `.erb` files to `ErbRunner`, and `.rb` files to `PhlexRunner`
|
|
23
23
|
- **Entry point:** `lib/a11y/lint.rb` — defines the `A11y::Lint` module and `A11y::Lint::Error` exception
|
|
24
|
-
- **Slim pipeline:** `SlimRunner` parses Slim templates via `Slim::Parser`; `
|
|
24
|
+
- **Slim pipeline:** `SlimRunner` parses Slim templates via `Slim::Parser`; `SlimNode` wraps Slim S-expressions
|
|
25
25
|
- **ERB pipeline:** `ErbRunner` parses ERB templates via Nokogiri; `ErbNode` wraps Nokogiri nodes and extracted `<%= %>` Ruby code
|
|
26
|
+
- **Phlex pipeline:** `PhlexRunner` parses Phlex view classes via Prism; `PhlexNode` wraps Prism AST call nodes; detects Phlex files by the presence of `def view_template`
|
|
27
|
+
- **Ruby code parsing:** `RubyCode` (`lib/a11y/lint/ruby_code.rb`) — parses a Ruby code string once with Prism and exposes the resulting `CallNode`; used by `SlimNode` and `ErbNode` so rules work against a parsed AST instead of re-parsing strings
|
|
26
28
|
- **Configuration:** `lib/a11y/lint/configuration.rb` — loads `.a11y-lint.yml` to enable/disable individual rules; searches upward from the target path
|
|
27
|
-
- **Rules:** `lib/a11y/lint/rules/` — organized by WCAG principle (`perceivable/`, `operable/`, `understandable/`, `robust/`); rules implement `check(node)` against the shared node interface (`tag_name`, `attribute?`, `attributes`, `
|
|
29
|
+
- **Rules:** `lib/a11y/lint/rules/` — organized by WCAG principle (`perceivable/`, `operable/`, `understandable/`, `robust/`); rules implement `check(node)` against the shared node interface (`tag_name`, `attribute?`, `attributes`, `call_node`, `line`)
|
|
28
30
|
- **Version:** `lib/a11y/lint/version.rb`
|
|
29
31
|
- **Type signatures (RBS):** `sig/a11y/lint.rbs`
|
|
30
32
|
- **Tests:** `test/` directory using Minitest; test helper at `test/test_helper.rb`
|
|
31
|
-
- **Dummy app:** `test/fixtures/dummy_app/` — a fixture app with Slim/ERB templates for end-to-end smoke testing before releases (`bundle exec a11y-lint test/fixtures/dummy_app`)
|
|
33
|
+
- **Dummy app:** `test/fixtures/dummy_app/` — a fixture app with Slim/ERB/Phlex templates for end-to-end smoke testing before releases (`bundle exec a11y-lint test/fixtures/dummy_app`)
|
|
32
34
|
|
|
33
35
|
## Code Style
|
|
34
36
|
|
|
35
37
|
RuboCop is configured in `.rubocop.yml`:
|
|
36
|
-
- Target Ruby version: 3.
|
|
38
|
+
- Target Ruby version: 3.3
|
|
37
39
|
- Enforces double quotes for strings
|
data/README.md
CHANGED
|
@@ -33,7 +33,7 @@ a11y-lint app/views/
|
|
|
33
33
|
a11y-lint app/views/home.html.slim app/views/about.html.slim
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
With no arguments, it scans the current directory recursively for `.slim
|
|
36
|
+
With no arguments, it scans the current directory recursively for `.slim`, `.erb`, and `.rb` (Phlex) files:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
a11y-lint
|
|
@@ -64,10 +64,21 @@ a11y-lint --config path/to/.a11y-lint.yml app/views/
|
|
|
64
64
|
```ruby
|
|
65
65
|
require "a11y/lint"
|
|
66
66
|
|
|
67
|
+
# Slim
|
|
67
68
|
source = File.read("app/views/home.html.slim")
|
|
68
|
-
runner = A11y::Lint::SlimRunner.new([A11y::Lint::Rules::ImgMissingAlt
|
|
69
|
+
runner = A11y::Lint::SlimRunner.new([A11y::Lint::Rules::ImgMissingAlt])
|
|
69
70
|
offenses = runner.run(source, filename: "app/views/home.html.slim")
|
|
70
71
|
|
|
72
|
+
# ERB
|
|
73
|
+
source = File.read("app/views/home.html.erb")
|
|
74
|
+
runner = A11y::Lint::ErbRunner.new([A11y::Lint::Rules::ImgMissingAlt])
|
|
75
|
+
offenses = runner.run(source, filename: "app/views/home.html.erb")
|
|
76
|
+
|
|
77
|
+
# Phlex
|
|
78
|
+
source = File.read("app/views/home_view.rb")
|
|
79
|
+
runner = A11y::Lint::PhlexRunner.new([A11y::Lint::Rules::ImgMissingAlt])
|
|
80
|
+
offenses = runner.run(source, filename: "app/views/home_view.rb")
|
|
81
|
+
|
|
71
82
|
offenses.each do |offense|
|
|
72
83
|
puts "#{offense.filename}:#{offense.line} [#{offense.rule}] #{offense.message}"
|
|
73
84
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A11y
|
|
4
|
+
module Lint
|
|
5
|
+
# Shared block-content queries for node types.
|
|
6
|
+
# Depends on the host class implementing
|
|
7
|
+
# #block_has_text_children? and #block_body_codes.
|
|
8
|
+
module BlockInspection
|
|
9
|
+
ICON_HELPERS = %w[inline_svg icon image_tag svg_icon].freeze
|
|
10
|
+
|
|
11
|
+
def block_has_only_icon_helpers?
|
|
12
|
+
return false if block_has_text_children?
|
|
13
|
+
|
|
14
|
+
codes = block_body_codes
|
|
15
|
+
return true unless codes&.any?
|
|
16
|
+
|
|
17
|
+
codes.all? { |code| icon_helper_call?(code) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def icon_helper_call?(code)
|
|
23
|
+
call = RubyCode.new(code).call_node
|
|
24
|
+
call && ICON_HELPERS.include?(call.method_name)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module A11y
|
|
6
|
+
module Lint
|
|
7
|
+
# Wraps a Prism::CallNode with a rule-friendly query API.
|
|
8
|
+
class CallNode
|
|
9
|
+
attr_reader :prism_node
|
|
10
|
+
|
|
11
|
+
def initialize(prism_node)
|
|
12
|
+
@prism_node = prism_node
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def method_name
|
|
16
|
+
@prism_node.name.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks for a keyword argument by name.
|
|
20
|
+
# keyword?(:alt) => alt: or "alt" =>
|
|
21
|
+
# keyword?(:aria, :label) => aria: { label: ... }
|
|
22
|
+
# keyword?(:"aria-label") => "aria-label" =>
|
|
23
|
+
def keyword?(*keys)
|
|
24
|
+
return false unless (kw_hash = find_keyword_hash)
|
|
25
|
+
|
|
26
|
+
if keys.length == 1
|
|
27
|
+
flat_keyword?(kw_hash, keys[0])
|
|
28
|
+
else
|
|
29
|
+
nested_keyword?(kw_hash, keys[0], keys[1])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def positional_args
|
|
34
|
+
return [] unless @prism_node.arguments
|
|
35
|
+
|
|
36
|
+
@prism_node.arguments.arguments.reject do |a|
|
|
37
|
+
a.is_a?(Prism::KeywordHashNode)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def first_positional_arg_empty_string?
|
|
42
|
+
first = positional_args.first
|
|
43
|
+
first.is_a?(Prism::StringNode) && first.unescaped.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def block?
|
|
47
|
+
!@prism_node.block.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Finds a receiverless call by method name in this node's
|
|
51
|
+
# subtree (including self). Returns a CallNode or nil.
|
|
52
|
+
def find(name)
|
|
53
|
+
found = search_for_call(@prism_node, name)
|
|
54
|
+
found ? self.class.new(found) : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def flat_keyword?(kw_hash, key)
|
|
60
|
+
kw_hash.elements.any? do |assoc|
|
|
61
|
+
key_name(assoc) == key.to_s
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def nested_keyword?(kw_hash, outer_key, inner_key)
|
|
66
|
+
assoc = kw_hash.elements.find do |a|
|
|
67
|
+
key_name(a) == outer_key.to_s
|
|
68
|
+
end
|
|
69
|
+
return false unless assoc&.value.is_a?(Prism::HashNode)
|
|
70
|
+
|
|
71
|
+
assoc.value.elements.any? do |inner|
|
|
72
|
+
key_name(inner) == inner_key.to_s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def find_keyword_hash
|
|
77
|
+
return unless @prism_node.arguments
|
|
78
|
+
|
|
79
|
+
@prism_node.arguments.arguments.find do |arg|
|
|
80
|
+
arg.is_a?(Prism::KeywordHashNode)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def key_name(assoc)
|
|
85
|
+
return unless assoc.is_a?(Prism::AssocNode)
|
|
86
|
+
|
|
87
|
+
case assoc.key
|
|
88
|
+
when Prism::SymbolNode then assoc.key.unescaped
|
|
89
|
+
when Prism::StringNode then assoc.key.unescaped
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def search_for_call(node, name)
|
|
94
|
+
if node.is_a?(Prism::CallNode) &&
|
|
95
|
+
node.receiver.nil? &&
|
|
96
|
+
node.name.to_s == name
|
|
97
|
+
return node
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
node.child_nodes.compact.each do |child|
|
|
101
|
+
found = search_for_call(child, name)
|
|
102
|
+
return found if found
|
|
103
|
+
end
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/a11y/lint/cli.rb
CHANGED
|
@@ -4,8 +4,7 @@ require "optparse"
|
|
|
4
4
|
|
|
5
5
|
module A11y
|
|
6
6
|
module Lint
|
|
7
|
-
# Command-line interface for running accessibility
|
|
8
|
-
# linting on Slim templates.
|
|
7
|
+
# Command-line interface for running accessibility linting.
|
|
9
8
|
class CLI
|
|
10
9
|
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
11
10
|
@argv = argv
|
|
@@ -19,7 +18,7 @@ module A11y
|
|
|
19
18
|
files = resolve_files(@argv)
|
|
20
19
|
|
|
21
20
|
if files.empty?
|
|
22
|
-
@stderr.puts("No .slim or .
|
|
21
|
+
@stderr.puts("No .slim, .erb, or .rb files found")
|
|
23
22
|
return 0
|
|
24
23
|
end
|
|
25
24
|
|
|
@@ -62,7 +61,7 @@ module A11y
|
|
|
62
61
|
|
|
63
62
|
def expand_path(path)
|
|
64
63
|
if File.directory?(path)
|
|
65
|
-
Dir.glob(File.join(path, "**", "*.{slim,erb}"))
|
|
64
|
+
Dir.glob(File.join(path, "**", "*.{slim,erb,rb}"))
|
|
66
65
|
elsif File.file?(path)
|
|
67
66
|
[path]
|
|
68
67
|
else
|
|
@@ -75,14 +74,23 @@ module A11y
|
|
|
75
74
|
rules = all_rules
|
|
76
75
|
slim_runner = SlimRunner.new(rules)
|
|
77
76
|
erb_runner = ErbRunner.new(rules)
|
|
77
|
+
phlex_runner = PhlexRunner.new(rules)
|
|
78
78
|
|
|
79
79
|
files.flat_map do |file|
|
|
80
80
|
source = File.read(file)
|
|
81
|
-
runner = file
|
|
81
|
+
runner = runner_for(file, slim_runner, erb_runner, phlex_runner)
|
|
82
82
|
runner.run(source, filename: file)
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
def runner_for(file, slim_runner, erb_runner, phlex_runner)
|
|
87
|
+
case File.extname(file)
|
|
88
|
+
when ".erb" then erb_runner
|
|
89
|
+
when ".rb" then phlex_runner
|
|
90
|
+
else slim_runner
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
def all_rules
|
|
87
95
|
configuration = Configuration.load(
|
|
88
96
|
@config_path,
|
data/lib/a11y/lint/erb_node.rb
CHANGED
|
@@ -5,16 +5,26 @@ module A11y
|
|
|
5
5
|
# Wraps a Nokogiri node or extracted ERB output tag
|
|
6
6
|
# as a queryable node for lint rules.
|
|
7
7
|
class ErbNode
|
|
8
|
-
|
|
8
|
+
include BlockInspection
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
attr_reader(:block_body_codes, :line, :ruby_code)
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
line:,
|
|
14
|
+
block_body_codes: nil,
|
|
15
|
+
block_has_text_children: false,
|
|
16
|
+
nokogiri_node: nil,
|
|
17
|
+
ruby_code: nil
|
|
18
|
+
)
|
|
13
19
|
@line = line
|
|
20
|
+
@block_body_codes = block_body_codes
|
|
21
|
+
@block_has_text_children = block_has_text_children
|
|
22
|
+
@nokogiri_node = nokogiri_node
|
|
23
|
+
@ruby_code = ruby_code
|
|
14
24
|
end
|
|
15
25
|
|
|
16
26
|
def tag_name
|
|
17
|
-
|
|
27
|
+
nokogiri_node&.name
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
def attribute?(name)
|
|
@@ -25,25 +35,31 @@ module A11y
|
|
|
25
35
|
@attributes ||= extract_attributes
|
|
26
36
|
end
|
|
27
37
|
|
|
28
|
-
def
|
|
29
|
-
@
|
|
38
|
+
def call_node
|
|
39
|
+
@call_node ||= RubyCode.new(ruby_code).call_node
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def block_has_text_children?
|
|
43
|
+
@block_has_text_children
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
# Returns direct element children wrapped as ErbNode objects.
|
|
33
47
|
def children
|
|
34
|
-
return [] unless
|
|
48
|
+
return [] unless nokogiri_node
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
nokogiri_node.element_children.map do |child|
|
|
37
51
|
ErbNode.new(nokogiri_node: child, line: child.line)
|
|
38
52
|
end
|
|
39
53
|
end
|
|
40
54
|
|
|
41
55
|
private
|
|
42
56
|
|
|
57
|
+
attr_reader(:nokogiri_node)
|
|
58
|
+
|
|
43
59
|
def extract_attributes
|
|
44
|
-
return {} unless
|
|
60
|
+
return {} unless nokogiri_node
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
nokogiri_node
|
|
47
63
|
.attributes
|
|
48
64
|
.each_with_object({}) do |(name, _attr), result|
|
|
49
65
|
result[name] = true
|
data/lib/a11y/lint/erb_runner.rb
CHANGED
|
@@ -53,12 +53,38 @@ module A11y
|
|
|
53
53
|
source.scan(ERB_OUTPUT_TAG) do
|
|
54
54
|
match = Regexp.last_match
|
|
55
55
|
code = match[1]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
line = source[0...match.begin(0)].count("\n") + 1
|
|
57
|
+
check_node(
|
|
58
|
+
build_erb_output_node(source, code, line, match.end(0))
|
|
59
|
+
)
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
def build_erb_output_node(source, code, line, match_end)
|
|
64
|
+
block_body_codes, block_has_text =
|
|
65
|
+
extract_block_info(source, code, match_end)
|
|
66
|
+
|
|
67
|
+
ErbNode.new(
|
|
68
|
+
ruby_code: code, line: line,
|
|
69
|
+
block_body_codes: block_body_codes,
|
|
70
|
+
block_has_text_children: block_has_text
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_block_info(source, code, match_end)
|
|
75
|
+
return [nil, false] unless code.match?(/\s+do\s*\z/)
|
|
76
|
+
|
|
77
|
+
rest = source[match_end..]
|
|
78
|
+
end_match = rest.match(/<%-?\s*end\s*-?%>/m)
|
|
79
|
+
return [nil, false] unless end_match
|
|
80
|
+
|
|
81
|
+
block_content = rest[0...end_match.begin(0)]
|
|
82
|
+
codes = block_content.scan(ERB_OUTPUT_TAG).map { |m| m[0].strip }
|
|
83
|
+
text_only = block_content.gsub(ERB_TAG, "").strip
|
|
84
|
+
|
|
85
|
+
[codes, !text_only.empty?]
|
|
86
|
+
end
|
|
87
|
+
|
|
62
88
|
def check_node(node)
|
|
63
89
|
rules.each do |rule_class|
|
|
64
90
|
message = rule_class.check(node)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A11y
|
|
4
|
+
module Lint
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when slim is not installed.
|
|
8
|
+
class SlimLoadError < Error
|
|
9
|
+
def initialize
|
|
10
|
+
super(
|
|
11
|
+
"a11y-lint needs the `slim` gem to lint .slim files. " \
|
|
12
|
+
"Add `gem \"slim\"` to your Gemfile."
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A11y
|
|
4
|
+
module Lint
|
|
5
|
+
# Wraps a Phlex HTML tag call or helper method call
|
|
6
|
+
# as a queryable node for lint rules.
|
|
7
|
+
class PhlexNode
|
|
8
|
+
include BlockInspection
|
|
9
|
+
extend PhlexTags
|
|
10
|
+
|
|
11
|
+
attr_reader(
|
|
12
|
+
:attributes,
|
|
13
|
+
:block_body_codes,
|
|
14
|
+
:call_node,
|
|
15
|
+
:children,
|
|
16
|
+
:line,
|
|
17
|
+
:tag_name
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# rubocop:disable Metrics/ParameterLists
|
|
21
|
+
def initialize(
|
|
22
|
+
line:, tag_name: nil, attributes: {},
|
|
23
|
+
call_node: nil, children: [],
|
|
24
|
+
block_body_codes: nil,
|
|
25
|
+
block_has_text_children: false
|
|
26
|
+
)
|
|
27
|
+
@tag_name = tag_name
|
|
28
|
+
@attributes = attributes
|
|
29
|
+
@call_node = call_node
|
|
30
|
+
@line = line
|
|
31
|
+
@children = children
|
|
32
|
+
@block_body_codes = block_body_codes
|
|
33
|
+
@block_has_text_children = block_has_text_children
|
|
34
|
+
end
|
|
35
|
+
# rubocop:enable Metrics/ParameterLists
|
|
36
|
+
|
|
37
|
+
def ruby_code
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attribute?(name)
|
|
42
|
+
attributes.key?(name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def block_has_text_children?
|
|
46
|
+
@block_has_text_children
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.build_tag(call_node, children: [])
|
|
50
|
+
name = call_node.name.to_s
|
|
51
|
+
new(
|
|
52
|
+
tag_name: html_tag_name(name),
|
|
53
|
+
attributes: extract_attributes(call_node),
|
|
54
|
+
line: call_node.location.start_line,
|
|
55
|
+
children: children
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.build_helper(
|
|
60
|
+
call_node,
|
|
61
|
+
block_body_codes: nil,
|
|
62
|
+
block_has_text_children: false
|
|
63
|
+
)
|
|
64
|
+
new(
|
|
65
|
+
call_node: CallNode.new(call_node),
|
|
66
|
+
line: call_node.location.start_line,
|
|
67
|
+
block_body_codes: block_body_codes,
|
|
68
|
+
block_has_text_children: block_has_text_children
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.extract_attributes(call_node)
|
|
73
|
+
return {} unless call_node.arguments
|
|
74
|
+
|
|
75
|
+
kwarg_nodes(call_node).each_with_object({}) do |elem, h|
|
|
76
|
+
key = kwarg_key(elem.key)
|
|
77
|
+
h[key] = true if key
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.kwarg_nodes(call_node)
|
|
82
|
+
args = call_node.arguments.arguments
|
|
83
|
+
args.select { |a| a.is_a?(Prism::KeywordHashNode) }
|
|
84
|
+
.flat_map { |a| a.elements.select { |e| e.is_a?(Prism::AssocNode) } }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.kwarg_key(key_node)
|
|
88
|
+
case key_node
|
|
89
|
+
when Prism::SymbolNode then key_node.value
|
|
90
|
+
when Prism::StringNode then key_node.unescaped
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private_class_method :kwarg_key, :kwarg_nodes,
|
|
95
|
+
:extract_attributes
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|