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
53 changes: 17 additions & 36 deletions Magento2/Sniffs/Html/HtmlClosingVoidTagsSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**
* Sniff for void closing tags.
*/
class HtmlClosingVoidTagsSniff implements Sniff
class HtmlClosingVoidTagsSniff extends HtmlSelfClosingTagsSniff implements Sniff
{
/**
* String representation of warning.
Expand All @@ -30,39 +30,6 @@ class HtmlClosingVoidTagsSniff implements Sniff
*/
private const WARNING_CODE = 'HtmlClosingVoidElements';

/**
* List of void elements.
*
* https://html.spec.whatwg.org/multipage/syntax.html#void-elements
*
* @var string[]
*/
private const HTML_VOID_ELEMENTS = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr'
];

/**
* @inheritdoc
*/
public function register(): array
{
return [T_INLINE_HTML];
}

/**
* Detect use of self-closing tag with void html element.
*
Expand All @@ -84,11 +51,25 @@ 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(
$ptr = $this->findPointer($phpcsFile, $match[0]);
$fix = $phpcsFile->addFixableWarning(
sprintf(self::WARNING_MESSAGE, $match[0]),
null,
$ptr,
self::WARNING_CODE
);

if ($fix) {
$token = $phpcsFile->getTokens()[$ptr];
$original = $token['content'];
$replacement = str_replace(' />', '>', $original);
$replacement = str_replace('/>', '>', $replacement);

if (preg_match('{^\s* />}', $original)) {
$replacement = ' ' . $replacement;
}

$phpcsFile->fixer->replaceToken($ptr, $replacement);
}
}
}
}
Expand Down
70 changes: 59 additions & 11 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 = [
protected 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 All @@ -55,7 +55,7 @@ public function register()
*
* @param File $phpcsFile
* @param int $stackPtr
* @return int|void
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
Expand All @@ -70,15 +70,63 @@ 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(
if (!in_array($match[1], self::HTML_VOID_ELEMENTS)) {
$ptr = $this->findPointer($phpcsFile, $match[0]);
$fix = $phpcsFile->addFixableError(
'Avoid using self-closing tag with non-void html element'
. ' - "' . $match[0] . PHP_EOL,
null,
. ' - "' . $match[0] . PHP_EOL,
$ptr,
'HtmlSelfClosingNonVoidTag'
);

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

if (preg_match('{^\s* />}', $original)) {
$replacement = ' ' . $replacement;
}

$phpcsFile->fixer->replaceToken($ptr, $replacement);
}
}
}
}
}

/**
* Apply a fix for the detected issue
*
* @param File $phpcsFile
* @param string $needle
* @return int|null
*/
protected function findPointer(File $phpcsFile, string $needle): ?int
{
if (str_contains($needle, "\n")) {
foreach (explode("\n", $needle) as $line) {
$result = $this->findPointer($phpcsFile, $line);
}
return $result;
}

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;
}
}
11 changes: 9 additions & 2 deletions Magento2/Tests/Html/HtmlClosingVoidTagsUnitTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@
<hr/>
<img src="" alt=""/>
<input type="text" id="test_input"/>
<keygen/>
<input type="text"
id="multi-line-input"
placeholder="Alert should be on last line"
/>
<input type="text"
id="multi-line-input2"
placeholder="Alert should be on last line" />
<link/>
<meta/>
<param name="" value=""/>
<video>
<source/>
<track src=""/>
</video>
<wbr/>
<hr/>
<hr style="color: red" />
</body>
</html>
42 changes: 42 additions & 0 deletions Magento2/Tests/Html/HtmlClosingVoidTagsUnitTest.inc.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
/**
* 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">
<input type="text"
id="multi-line-input"
placeholder="Alert should be on last line"
>
<input type="text"
id="multi-line-input2"
placeholder="Alert should be on last line">
<link>
<meta>
<video>
<source>
<track src="">
</video>
<wbr>
<hr>
<hr style="color: red">
</body>
</html>
21 changes: 20 additions & 1 deletion Magento2/Tests/Html/HtmlClosingVoidTagsUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ 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,
28 => 1,
31 => 1,
32 => 1,
33 => 1,
35 => 1,
36 => 1,
38 => 1,
39 => 1,
40 => 1,
];
}
}
16 changes: 14 additions & 2 deletions Magento2/Tests/Html/HtmlSelfClosingTagsUnitTest.1.inc
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,27 @@
<hr/>
<img src="" alt=""/>
<input type="text" id="test_input"/>
<keygen/>
<input type="text"
id="multi-line-input"
placeholder="Alert should be on last line"
/>
<input type="text"
id="multi-line-input2"
placeholder="Alert should be on last line" />
<link/>
<meta/>
<param name="" value=""/>
<video>
<source/>
<track src=""/>
</video>
<wbr/>

<label for="test_input"/>
<label
for="multi-line-input"
/>
<label
for="multi-line-input2" />
<style type="text/css"/>
<div/>
<span/>
Expand All @@ -41,5 +51,7 @@
<each/>
<translate/>
<scope/>
<span/>
<span style="color: red" />
</body>
</html>
57 changes: 57 additions & 0 deletions Magento2/Tests/Html/HtmlSelfClosingTagsUnitTest.1.inc.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!--
/**
* 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"/>
<input type="text"
id="multi-line-input"
placeholder="Alert should be on last line"
/>
<input type="text"
id="multi-line-input2"
placeholder="Alert should be on last line" />
<link/>
<meta/>
<video>
<source/>
<track src=""/>
</video>
<wbr/>

<label for="test_input"></label>
<label
for="multi-line-input"
></label>
<label
for="multi-line-input2"></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>
Loading