Skip to content

Commit 73181d9

Browse files
committed
AC-201: Move phpcs checks from magento2 to magento-coding-standard repo
- Move files from Html folder
1 parent 3d68361 commit 73181d9

File tree

2 files changed

+348
-0
lines changed

2 files changed

+348
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Sniffs\Html;
8+
9+
use PHP_CodeSniffer\Sniffs\Sniff;
10+
use PHP_CodeSniffer\Files\File;
11+
12+
/**
13+
* Sniffing improper HTML bindings.
14+
*/
15+
class HtmlBindingSniff implements Sniff
16+
{
17+
/**
18+
* @inheritDoc
19+
*/
20+
public function register()
21+
{
22+
return [T_INLINE_HTML];
23+
}
24+
25+
/**
26+
* Load HTML document to validate.
27+
*
28+
* @param int $stackPointer
29+
* @param File $file
30+
* @return \DOMDocument|null
31+
*/
32+
private function loadHtmlDocument(int $stackPointer, File $file): ?\DOMDocument
33+
{
34+
if ($stackPointer === 0) {
35+
$html = $file->getTokensAsString($stackPointer, count($file->getTokens()));
36+
$dom = new \DOMDocument();
37+
try {
38+
// phpcs:disable Generic.PHP.NoSilencedErrors
39+
@$dom->loadHTML($html);
40+
return $dom;
41+
} catch (\Throwable $exception) {
42+
return null;
43+
}
44+
}
45+
46+
return null;
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*
52+
* Find HTML data bindings and check variables used.
53+
*/
54+
public function process(File $phpcsFile, $stackPtr)
55+
{
56+
if (!$dom = $this->loadHtmlDocument($stackPtr, $phpcsFile)) {
57+
return;
58+
}
59+
60+
/** @var string[] $htmlBindings */
61+
$htmlBindings = [];
62+
$domXpath = new \DOMXPath($dom);
63+
$dataBindAttributes = $domXpath->query('//@*[name() = "data-bind"]');
64+
foreach ($dataBindAttributes as $dataBindAttribute) {
65+
$knockoutBinding = $dataBindAttribute->nodeValue;
66+
preg_match('/^(.+\s*?)?html\s*?\:(.+)/ims', $knockoutBinding, $htmlBindingStart);
67+
if ($htmlBindingStart) {
68+
$htmlBinding = trim(preg_replace('/\,[a-z0-9\_\s]+\:.+/ims', '', $htmlBindingStart[2]));
69+
$htmlBindings[] = $htmlBinding;
70+
}
71+
}
72+
$htmlAttributes = $domXpath->query('//@*[name() = "html"]');
73+
foreach ($htmlAttributes as $htmlAttribute) {
74+
$magentoBinding = $htmlAttribute->nodeValue;
75+
$htmlBindings[] = trim($magentoBinding);
76+
}
77+
foreach ($htmlBindings as $htmlBinding) {
78+
if (!preg_match('/^[0-9\\\'\"]/ims', $htmlBinding)
79+
&& !preg_match('/UnsanitizedHtml(\(.*?\))*?$/', $htmlBinding)
80+
) {
81+
$phpcsFile->addError(
82+
'Variables/functions used for HTML binding must have UnsanitizedHtml suffix'
83+
. ' - "' . $htmlBinding . '" doesn\'t,' . PHP_EOL
84+
. 'consider using text binding if the value is supposed to be text',
85+
null,
86+
'UIComponentTemplate.KnockoutBinding.HtmlSuffix'
87+
);
88+
}
89+
}
90+
}
91+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Sniffs\Html;
10+
11+
use Magento\Framework\Filter\Template;
12+
use PHP_CodeSniffer\Sniffs\Sniff;
13+
use PHP_CodeSniffer\Files\File;
14+
15+
/**
16+
* Sniff for invalid directive usage in HTML templates
17+
*/
18+
class HtmlDirectiveSniff implements Sniff
19+
{
20+
/**
21+
* @var array
22+
*/
23+
private $usedVariables = [];
24+
25+
/**
26+
* @var array
27+
*/
28+
private $unfilteredVariables = [];
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public function register()
34+
{
35+
return [T_INLINE_HTML];
36+
}
37+
38+
/**
39+
* Detect invalid usage of template filter directives
40+
*
41+
* @param File $phpcsFile
42+
* @param int $stackPtr
43+
* @return int|void
44+
*/
45+
public function process(File $phpcsFile, $stackPtr)
46+
{
47+
$this->usedVariables = [];
48+
$this->unfilteredVariables = [];
49+
if ($stackPtr !== 0) {
50+
return;
51+
}
52+
53+
$html = $phpcsFile->getTokensAsString($stackPtr, count($phpcsFile->getTokens()));
54+
55+
if (empty($html)) {
56+
return;
57+
}
58+
59+
$html = $this->processIfDirectives($html, $phpcsFile);
60+
$html = $this->processDependDirectives($html, $phpcsFile);
61+
$html = $this->processForDirectives($html, $phpcsFile);
62+
$html = $this->processVarDirectivesAndParams($html, $phpcsFile);
63+
64+
$this->validateDefinedVariables($phpcsFile, $html);
65+
}
66+
67+
/**
68+
* Process the {{if}} directives in the file
69+
*
70+
* @param string $html
71+
* @param File $phpcsFile
72+
* @return string The processed template
73+
*/
74+
private function processIfDirectives(string $html, File $phpcsFile): string
75+
{
76+
if (preg_match_all(Template::CONSTRUCTION_IF_PATTERN, $html, $constructions, PREG_SET_ORDER)) {
77+
foreach ($constructions as $construction) {
78+
// validate {{if <var>}}
79+
$this->validateVariableUsage($phpcsFile, $construction[1]);
80+
$html = str_replace($construction[0], $construction[2] . ($construction[4] ?? ''), $html);
81+
}
82+
}
83+
84+
return $html;
85+
}
86+
87+
/**
88+
* Process the {{depend}} directives in the file
89+
*
90+
* @param string $html
91+
* @param File $phpcsFile
92+
* @return string The processed template
93+
*/
94+
private function processDependDirectives(string $html, File $phpcsFile): string
95+
{
96+
if (preg_match_all(Template::CONSTRUCTION_DEPEND_PATTERN, $html, $constructions, PREG_SET_ORDER)) {
97+
foreach ($constructions as $construction) {
98+
// validate {{depend <var>}}
99+
$this->validateVariableUsage($phpcsFile, $construction[1]);
100+
$html = str_replace($construction[0], $construction[2], $html);
101+
}
102+
}
103+
104+
return $html;
105+
}
106+
107+
/**
108+
* Process the {{for}} directives in the file
109+
*
110+
* @param string $html
111+
* @param File $phpcsFile
112+
* @return string The processed template
113+
*/
114+
private function processForDirectives(string $html, File $phpcsFile): string
115+
{
116+
if (preg_match_all(Template::LOOP_PATTERN, $html, $constructions, PREG_SET_ORDER)) {
117+
foreach ($constructions as $construction) {
118+
// validate {{for in <var>}}
119+
$this->validateVariableUsage($phpcsFile, $construction['loopData']);
120+
$html = str_replace($construction[0], $construction['loopBody'], $html);
121+
}
122+
}
123+
124+
return $html;
125+
}
126+
127+
/**
128+
* Process the all var directives and var directive params in the file
129+
*
130+
* @param string $html
131+
* @param File $phpcsFile
132+
* @return string The processed template
133+
*/
134+
private function processVarDirectivesAndParams(string $html, File $phpcsFile): string
135+
{
136+
if (preg_match_all(Template::CONSTRUCTION_PATTERN, $html, $constructions, PREG_SET_ORDER)) {
137+
foreach ($constructions as $construction) {
138+
if (empty($construction[2])) {
139+
continue;
140+
}
141+
142+
if ($construction[1] === 'var') {
143+
$this->validateVariableUsage($phpcsFile, $construction[2]);
144+
} else {
145+
$this->validateDirectiveBody($phpcsFile, $construction[2]);
146+
}
147+
}
148+
}
149+
150+
return $html;
151+
}
152+
153+
/**
154+
* Validate directive body is valid. e.g. {{somedir <directive body>}}
155+
*
156+
* @param File $phpcsFile
157+
* @param string $body
158+
*/
159+
private function validateDirectiveBody(File $phpcsFile, string $body): void
160+
{
161+
$parameterTokenizer = new Template\Tokenizer\Parameter();
162+
$parameterTokenizer->setString($body);
163+
$params = $parameterTokenizer->tokenize();
164+
165+
foreach ($params as $param) {
166+
if (substr($param, 0, 1) === '$') {
167+
$this->validateVariableUsage($phpcsFile, substr($param, 1));
168+
}
169+
}
170+
}
171+
172+
/**
173+
* Validate directive variable usage is valid. e.g. {{var <variable body>}} or {{somedir some_param="$foo.bar()"}}
174+
*
175+
* @param File $phpcsFile
176+
* @param string $body
177+
*/
178+
private function validateVariableUsage(File $phpcsFile, string $body): void
179+
{
180+
$this->usedVariables[] = 'var ' . trim($body);
181+
if (strpos($body, '|') !== false) {
182+
$this->unfilteredVariables[] = 'var ' . trim(explode('|', $body, 2)[0]);
183+
}
184+
$variableTokenizer = new Template\Tokenizer\Variable();
185+
$variableTokenizer->setString($body);
186+
$stack = $variableTokenizer->tokenize();
187+
188+
if (empty($stack)) {
189+
return;
190+
}
191+
192+
foreach ($stack as $token) {
193+
// As a static analyzer there are no data types to know if this is a DataObject so allow all get* methods
194+
if ($token['type'] === 'method' && substr($token['name'], 0, 3) !== 'get') {
195+
$phpcsFile->addError(
196+
'Template directives may not invoke methods. Only scalar array access is allowed.' . PHP_EOL
197+
. 'Found "' . trim($body) . '"',
198+
null,
199+
'HtmlTemplates.DirectiveUsage.ProhibitedMethodCall'
200+
);
201+
}
202+
}
203+
}
204+
205+
/**
206+
* Validate the variables defined in the template comment block match the variables actually used in the template
207+
*
208+
* @param File $phpcsFile
209+
* @param string $templateText
210+
*/
211+
private function validateDefinedVariables(File $phpcsFile, string $templateText): void
212+
{
213+
preg_match('/<!--@vars\s*((?:.)*?)\s*@-->/us', $templateText, $matches);
214+
215+
$definedVariables = [];
216+
217+
if (!empty($matches[1])) {
218+
$definedVariables = json_decode(str_replace("\n", '', $matches[1]), true);
219+
if (json_last_error()) {
220+
$phpcsFile->addError(
221+
'Template @vars comment block contains invalid JSON.',
222+
null,
223+
'HtmlTemplates.DirectiveUsage.InvalidVarsJSON'
224+
);
225+
return;
226+
}
227+
228+
foreach ($definedVariables as $var => $label) {
229+
if (empty($label)) {
230+
$phpcsFile->addError(
231+
'Template @vars comment block contains invalid label.' . PHP_EOL
232+
. 'Label for variable "' . $var . '" is empty.',
233+
null,
234+
'HtmlTemplates.DirectiveUsage.InvalidVariableLabel'
235+
);
236+
}
237+
}
238+
239+
$definedVariables = array_keys($definedVariables);
240+
foreach ($definedVariables as $definedVariable) {
241+
if (strpos($definedVariable, '|') !== false) {
242+
$definedVariables[] = trim(explode('|', $definedVariable, 2)[0]);
243+
}
244+
}
245+
}
246+
247+
$undefinedVariables = array_diff($this->usedVariables, $definedVariables, $this->unfilteredVariables);
248+
foreach ($undefinedVariables as $undefinedVariable) {
249+
$phpcsFile->addError(
250+
'Template @vars comment block is missing a variable used in the template.' . PHP_EOL
251+
. 'Missing variable: ' . $undefinedVariable,
252+
null,
253+
'HtmlTemplates.DirectiveUsage.UndefinedVariable'
254+
);
255+
}
256+
}
257+
}

0 commit comments

Comments
 (0)