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

Improve developer experience with closing tags #435

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
66 changes: 57 additions & 9 deletions Magento2/Sniffs/Html/HtmlClosingVoidTagsSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,18 @@ class HtmlClosingVoidTagsSniff implements Sniff
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr'
'wbr',
];

/** @var int */
private int $lastPointer = 0;

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -84,13 +85,60 @@ public function process(File $phpcsFile, $stackPtr): void
if (preg_match_all('$<(\w{2,})\s?[^<]*\/>$', $html, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if (in_array($match[1], self::HTML_VOID_ELEMENTS)) {
$phpcsFile->addWarning(
sprintf(self::WARNING_MESSAGE, $match[0]),
null,
self::WARNING_CODE
);
$ptr = $this->findPointer($phpcsFile, $match[0]);
if ($ptr) {
$fix = $phpcsFile->addFixableWarning(
sprintf(self::WARNING_MESSAGE, $match[0]),
$ptr,
self::WARNING_CODE
);

if ($fix) {
$token = $phpcsFile->getTokens()[$ptr];
$original = $match[0];
$replacement = str_replace(' />', '>', $original);
$replacement = str_replace('/>', '>', $replacement);
$phpcsFile->fixer->replaceToken(
$ptr,
str_replace($original, $replacement, $token['content'])
);
}
} else {
$phpcsFile->addWarning(
sprintf(self::WARNING_MESSAGE, $match[0]),
null,
self::WARNING_CODE
);
}
}
}
}
}

/**
* Apply a fix for the detected issue
*
* @param File $phpcsFile
* @param string $needle
* @return int|null
*/
public function findPointer(File $phpcsFile, string $needle): ?int
{
foreach ($phpcsFile->getTokens() as $ptr => $token) {
if ($ptr < $this->lastPointer) {
continue;
}

if ($token['code'] !== T_INLINE_HTML) {
continue;
}

if (str_contains($token['content'], $needle)) {
$this->lastPointer = $ptr;
return $ptr;
}
}

return null;
}
}
75 changes: 62 additions & 13 deletions Magento2/Sniffs/Html/HtmlSelfClosingTagsSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
class HtmlSelfClosingTagsSniff implements Sniff
{
/**
* List of void elements
* List of void elements.
*
* https://www.w3.org/TR/html51/syntax.html#writing-html-documents-elements
* https://html.spec.whatwg.org/multipage/syntax.html#void-elements
*
* @var string[]
*/
private $voidElements = [
private const HTML_VOID_ELEMENTS = [
'area',
'base',
'br',
Expand All @@ -32,16 +32,16 @@ class HtmlSelfClosingTagsSniff implements Sniff
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr',
];

/** @var int */
private int $lastPointer = 0;

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -70,15 +70,64 @@ public function process(File $phpcsFile, $stackPtr)

if (preg_match_all('$<(\w{2,})\s?[^<]*\/>$', $html, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if (!in_array($match[1], $this->voidElements)) {
$phpcsFile->addError(
'Avoid using self-closing tag with non-void html element'
. ' - "' . $match[0] . PHP_EOL,
null,
'HtmlSelfClosingNonVoidTag'
);
if (!in_array($match[1], self::HTML_VOID_ELEMENTS)) {
$ptr = $this->findPointer($phpcsFile, $match[0]);
if ($ptr) {
$fix = $phpcsFile->addFixableError(
'Avoid using self-closing tag with non-void html element'
. ' - "' . $match[0] . PHP_EOL,
$ptr,
'HtmlSelfClosingNonVoidTag'
);

if ($fix) {
$token = $phpcsFile->getTokens()[$ptr];
$original = $match[0];
$replacement = str_replace(' />', '></' . $match[1] . '>', $original);
$replacement = str_replace('/>', '></' . $match[1] . '>', $replacement);
$phpcsFile->fixer->replaceToken(
$ptr,
str_replace($original, $replacement, $token['content'])
);

}
} else {
$phpcsFile->addError(
'Avoid using self-closing tag with non-void html element'
. ' - "' . $match[0] . PHP_EOL,
null,
'HtmlSelfClosingNonVoidTag'
);
}
}
}
}
}

/**
* Apply a fix for the detected issue
*
* @param File $phpcsFile
* @param string $needle
* @return int|null
*/
private function findPointer(File $phpcsFile, string $needle): ?int
{
foreach ($phpcsFile->getTokens() as $ptr => $token) {
if ($ptr < $this->lastPointer) {
continue;
}

if ($token['code'] !== T_INLINE_HTML) {
continue;
}

if (str_contains($token['content'], $needle)) {
$this->lastPointer = $ptr;
return $ptr;
}
}

return null;
}
}
4 changes: 2 additions & 2 deletions Magento2/Tests/Html/HtmlClosingVoidTagsUnitTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
<hr/>
<img src="" alt=""/>
<input type="text" id="test_input"/>
<keygen/>
<link/>
<meta/>
<param name="" value=""/>
<video>
<source/>
<track src=""/>
</video>
<wbr/>
<hr/>
<hr style="color: red" />
</body>
</html>
35 changes: 35 additions & 0 deletions Magento2/Tests/Html/HtmlClosingVoidTagsUnitTest.inc.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->

<html>
<head>
<base>
<link>
</head>
<body>
<area alt="">
<br>
<table>
<colgroup>
<col>
</colgroup>
</table>
<embed>
<hr>
<img src="" alt="">
<input type="text" id="test_input">
<link>
<meta>
<video>
<source>
<track src="">
</video>
<wbr>
<hr>
<hr style="color: red">
</body>
</html>
19 changes: 18 additions & 1 deletion Magento2/Tests/Html/HtmlClosingVoidTagsUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ public function getErrorList()
*/
public function getWarningList()
{
return [1 => 15];
return [
10 => 1,
11 => 1,
14 => 1,
15 => 1,
18 => 1,
21 => 1,
22 => 1,
23 => 1,
24 => 1,
25 => 1,
26 => 1,
28 => 1,
29 => 1,
31 => 1,
32 => 1,
33 => 1,
];
}
}
4 changes: 2 additions & 2 deletions Magento2/Tests/Html/HtmlSelfClosingTagsUnitTest.1.inc
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@
<hr/>
<img src="" alt=""/>
<input type="text" id="test_input"/>
<keygen/>
<link/>
<meta/>
<param name="" value=""/>
<video>
<source/>
<track src=""/>
Expand All @@ -41,5 +39,7 @@
<each/>
<translate/>
<scope/>
<span/>
<span style="color: red" />
</body>
</html>
45 changes: 45 additions & 0 deletions Magento2/Tests/Html/HtmlSelfClosingTagsUnitTest.1.inc.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->

<html>
<head>
<base/>
<link/>
</head>
<body>
<area alt=""/>
<br/>
<table>
<colgroup>
<col/>
</colgroup>
</table>
<embed/>
<hr/>
<img src="" alt=""/>
<input type="text" id="test_input"/>
<link/>
<meta/>
<video>
<source/>
<track src=""/>
</video>
<wbr/>

<label for="test_input"></label>
<style type="text/css"></style>
<div></div>
<span></span>
<text></text>
<render></render>
<each></each>
<translate></translate>
<scope></scope>
<span></span>
<span style="color: red"></span>
</body>
</html>
14 changes: 13 additions & 1 deletion Magento2/Tests/Html/HtmlSelfClosingTagsUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@ class HtmlSelfClosingTagsUnitTest extends AbstractSniffUnitTest
*/
public function getErrorList()
{
return [1 => 9];
return [
33 => 1,
34 => 1,
35 => 1,
36 => 1,
37 => 1,
38 => 1,
39 => 1,
40 => 1,
41 => 1,
42 => 1,
43 => 1,
];
}

/**
Expand Down