Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SONARHTML-207 Create rule S6827: Anchors should contain accessible content #267

Merged
merged 4 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
549 changes: 549 additions & 0 deletions its/ruling/src/test/resources/expected/Web-S6827.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* SonarSource HTML analyzer :: Sonar Plugin
* Copyright (c) 2010-2024 SonarSource SA and Matthijs Galesloot
* [email protected]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sonar.plugins.html.checks.accessibility;

import java.util.ArrayDeque;
import java.util.Deque;
import org.sonar.check.Rule;
import org.sonar.plugins.html.checks.AbstractPageCheck;
import org.sonar.plugins.html.node.DirectiveNode;
import org.sonar.plugins.html.node.ExpressionNode;
import org.sonar.plugins.html.node.TagNode;
import org.sonar.plugins.html.node.TextNode;

@Rule(key = "S6827")
public class AnchorsHaveContentCheck extends AbstractPageCheck {

private static final String MESSAGE = "Anchors must have content and the content must be accessible by a screen reader.";

private Deque<Anchor> anchors = new ArrayDeque<>();

private static class Anchor {
final TagNode node;
boolean hasContent;

private Anchor(TagNode node, boolean hasContent) {
this.node = node;
this.hasContent = hasContent;
}
}

@Override
public void startElement(TagNode element) {
if (isAnchor(element)) {
anchors.push(new Anchor(element, hasContent(element)));
}
}

@Override
public void endElement(TagNode element) {
if (isAnchor(element) && !anchors.isEmpty()) {
var anchor = anchors.pop();
if (!anchor.hasContent) {
createViolation(anchor.node.getStartLinePosition(), MESSAGE);
}
}
}

@Override
public void endDocument() {
anchors.clear();
}

@Override
public void characters(TextNode node) {
if (!anchors.isEmpty()) {
var anchor = anchors.peek();
anchor.hasContent = anchor.hasContent || !node.isBlank();
}
}

@Override
public void directive(DirectiveNode node) {
if (!anchors.isEmpty()) {
var anchor = anchors.peek();
anchor.hasContent = anchor.hasContent || "?php".equals(node.getNodeName());
}
}

@Override
public void expression(ExpressionNode node) {
if (!anchors.isEmpty()) {
var anchor = anchors.peek();
anchor.hasContent = true;
}
}

private static boolean isAnchor(TagNode element) {
return "a".equalsIgnoreCase(element.getNodeName());
}

private static boolean hasContent(TagNode element) {
var children = element.getChildren();
for (TagNode child : children) {
if (!isHidden(child)) {
return true;
}
}
return element.hasProperty("title") || element.hasProperty("aria-label");
}

private static boolean isHidden(TagNode element) {
return ("input".equalsIgnoreCase(element.getNodeName())
ilia-kebets-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
&& "hidden".equalsIgnoreCase(element.getPropertyValue("type")))
|| "true".equalsIgnoreCase(element.getPropertyValue("aria-hidden"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package org.sonar.plugins.html.rules;

import java.util.List;

import org.sonar.plugins.html.checks.accessibility.AnchorsHaveContentCheck;
import org.sonar.plugins.html.checks.attributes.IllegalAttributeCheck;
import org.sonar.plugins.html.checks.attributes.RequiredAttributeCheck;
import org.sonar.plugins.html.checks.coding.ComplexityCheck;
Expand Down Expand Up @@ -89,6 +91,7 @@ public final class CheckClasses {

private static final List<Class<?>> CLASSES = List.of(
AbsoluteURICheck.class,
AnchorsHaveContentCheck.class,
AvoidHtmlCommentCheck.class,
ChildElementRequiredCheck.class,
ComplexityCheck.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<h2>Why is this an issue?</h2>
<p>Anchors, represented by the <code>a</code> tag in HTML, usually contain a hyperlink that users can click to navigate to different sections of a
website or different websites altogether.</p>
<p>However, when anchors do not have content or when the content is hidden from screen readers using the <code>aria-hidden</code> property, it creates
a significant accessibility issue. If an anchor’s content is hidden or non-existent, visually impaired users may not be able to understand the purpose
of the anchor or navigate the website effectively.</p>
<p>This rule checks that anchors do not use the <code>aria-hidden</code> property and have content provided either between the tags or as
<code>aria-label</code> or <code>title</code> property.</p>
<h2>How to fix it</h2>
<p>Ensure that anchors either have content or an <code>aria-label</code> or <code>title</code> attribute, and they should not use the
<code>aria-hidden</code> property.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
&lt;a aria-hidden&gt;link to my site&lt;/a&gt;
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
&lt;a&gt;link to my site&lt;/a&gt;
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li> MDN web docs - <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"><code>&lt;a&gt;</code>: The Anchor element</a> </li>
<li> MDN web docs - <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden"><code>aria-hidden</code>
attribute</a> </li>
</ul>
<h3>Standards</h3>
<ul>
<li> W3C - <a href="https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context">Link purpose</a> </li>
</ul>

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"title": "Anchors should contain accessible content",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"accessibility"
],
"defaultSeverity": "Minor",
"ruleSpecification": "RSPEC-6827",
"sqKey": "S6827",
"scope": "All",
"quickfix": "infeasible",
"code": {
"impacts": {
"MAINTAINABILITY": "LOW",
"RELIABILITY": "LOW"
},
"attribute": "CONVENTIONAL"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"S5260",
"S5264",
"S5725",
"S6827",
"ServerSideImageMapsCheck",
"TableHeaderHasIdOrScopeCheck",
"TableWithoutCaptionCheck",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SonarSource HTML analyzer :: Sonar Plugin
* Copyright (c) 2010-2024 SonarSource SA and Matthijs Galesloot
* [email protected]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sonar.plugins.html.checks.accessibility;

import java.io.File;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.sonar.plugins.html.checks.CheckMessagesVerifierRule;
import org.sonar.plugins.html.checks.TestHelper;
import org.sonar.plugins.html.visitor.HtmlSourceCode;

class AnchorsHaveContentCheckTest {

@RegisterExtension
public CheckMessagesVerifierRule checkMessagesVerifier = new CheckMessagesVerifierRule();

@Test
void html() throws Exception {
HtmlSourceCode sourceCode = TestHelper.scan(
new File("src/test/resources/checks/AnchorsHaveContentCheck.html"),
new AnchorsHaveContentCheck());

checkMessagesVerifier.verify(sourceCode.getIssues())
.next().atLine(1).withMessage("Anchors must have content and the content must be accessible by a screen reader.")
.next().atLine(2)
.next().atLine(3)
.next().atLine(4)
.noMore();
}

@Test
void php() throws Exception {
HtmlSourceCode sourceCode = TestHelper.scan(
new File("src/test/resources/checks/AnchorsHaveContentCheck.php"),
new AnchorsHaveContentCheck());

checkMessagesVerifier.verify(sourceCode.getIssues())
.next().atLine(1)
.next().atLine(2)
.noMore();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<a></a> <!-- Noncompliant -->
<a> <!-- blank--> </a> <!-- Noncompliant -->
<a><input type="hidden"/></a> <!-- Noncompliant -->
<a><button aria-hidden="true"></button></a> <!-- Noncompliant -->

<a>Click me</a>
<a title="Click me"></a>
<a aria-label="Click me"></a>
<a><img src="img.jpg"/></a>
<a><input type="text"/></a>
<a><button aria-hidden="foo"></button></a>
<a><button aria-hidden="false"></button></a>
<a><% "hello" %></a>
<a title="Click me"><% "hello" %></a>
<a><img src="img.jpg"/>Click me</a>
<div></div>
</a> <!-- malformed on purpose -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<a></a> <!-- Noncompliant -->
<a><?foo "bar" ?></a> <!-- Noncompliant -->
<a><?php echo "Click me"; ?></a>
<a title="Click me"><?php echo "Click me"; ?></a>