Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions src/Analyser/FileAnalysisProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Boundwize\StructArmed\Analyser;

use Boundwize\StructArmed\Util\InlineHtmlOpeningTagMatcher;
use Boundwize\StructArmed\Util\Path;
use PhpParser\Error;
use PhpParser\Node;
Expand Down Expand Up @@ -33,7 +34,6 @@
use function token_get_all;
use function trim;

use const PREG_OFFSET_CAPTURE;
use const T_INLINE_HTML;
use const T_OPEN_TAG;
use const T_OPEN_TAG_WITH_ECHO;
Expand Down Expand Up @@ -221,12 +221,12 @@ private function invalidPhpTagLineForToken(int $id, string $text, int $tokenLine

if (
$id !== T_INLINE_HTML
|| preg_match('/<\?(?!php(?:\s|$)|=)/', $text, $matches, PREG_OFFSET_CAPTURE) !== 1
|| ($tagOffset = InlineHtmlOpeningTagMatcher::invalidInlineHtmlTagOffset($text)) === null
) {
return null;
}

return $tokenLine + substr_count(substr($text, 0, $matches[0][1]), "\n");
return $tokenLine + substr_count(substr($text, 0, $tagOffset), "\n");
}

private function contents(string $file): string
Expand Down
7 changes: 3 additions & 4 deletions src/Rule/Rules/File/Psr1PhpTagsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Boundwize\StructArmed\Rule\FileAnalysisRuleInterface;
use Boundwize\StructArmed\Rule\FixableInterface;
use Boundwize\StructArmed\Rule\RuleViolation;
use Boundwize\StructArmed\Util\InlineHtmlOpeningTagMatcher;

use function file_get_contents;
use function file_put_contents;
Expand All @@ -21,7 +22,6 @@
use function substr_count;
use function token_get_all;

use const PREG_OFFSET_CAPTURE;
use const T_INLINE_HTML;
use const T_OPEN_TAG;

Expand Down Expand Up @@ -150,9 +150,8 @@ private function replacementForInvalidTag(int $id, string $text, int $tokenLine,

$searchOffset = 0;

while (preg_match('/<\?(?!php(?:\s|$)|=)/', $text, $matches, PREG_OFFSET_CAPTURE, $searchOffset) === 1) {
$tagOffset = $matches[0][1];
$tagLine = $tokenLine + substr_count(substr($text, 0, $tagOffset), "\n");
while (($tagOffset = InlineHtmlOpeningTagMatcher::invalidInlineHtmlTagOffset($text, $searchOffset)) !== null) {
$tagLine = $tokenLine + substr_count(substr($text, 0, $tagOffset), "\n");

if ($tagLine === $targetLine) {
return [
Expand Down
41 changes: 41 additions & 0 deletions src/Util/InlineHtmlOpeningTagMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Boundwize\StructArmed\Util;

use function array_map;
use function implode;
use function preg_match;
use function preg_quote;
use function sprintf;

use const PREG_OFFSET_CAPTURE;

final class InlineHtmlOpeningTagMatcher
{
/** @var list<string> */
private const ALLOWED_TAGS = [
'php',
'xml',
'xml-stylesheet',
];

public static function invalidInlineHtmlTagOffset(string $text, int $offset = 0): ?int
{
$allowedTags = implode('|', array_map(
static fn (string $target): string => preg_quote($target, '/'),
self::ALLOWED_TAGS,
));
$pattern = sprintf(
'/<\?(?!(?:%s)(?:\s|$|\?>)|=)/',
$allowedTags,
);

if (preg_match($pattern, $text, $matches, PREG_OFFSET_CAPTURE, $offset) !== 1) {
return null;
}

return $matches[0][1];
}
}
6 changes: 6 additions & 0 deletions tests/Analyser/FileAnalysisProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Boundwize\StructArmed\Analyser\FileAnalysis;
use Boundwize\StructArmed\Analyser\FileAnalysisProvider;
use Boundwize\StructArmed\Util\InlineHtmlOpeningTagMatcher;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
Expand All @@ -14,6 +15,7 @@

#[CoversClass(FileAnalysis::class)]
#[CoversClass(FileAnalysisProvider::class)]
#[CoversClass(InlineHtmlOpeningTagMatcher::class)]
final class FileAnalysisProviderTest extends TestCase
{
public function testAnalysesPsr1FactsAndCachesThemByFile(): void
Expand Down Expand Up @@ -92,8 +94,12 @@ public static function lightweightAnalysisProvider(): iterable
yield 'invalid UTF-8' => ["<?php echo \"\xB1\";", false, false, null];
yield 'short tag' => ['<? echo "short";', false, true, 1];
yield 'uppercase tag' => ['<?PHP echo "upper";', false, true, 1];
yield 'PHP-like inline HTML' => ['<?php?>', false, true, null];
yield 'valid tag' => ['<?php echo "valid";', false, true, null];
yield 'echo tag' => ['<?= "echo";', false, true, null];
yield 'XML declaration' => ['<?xml version="1.0"?>', false, true, null];
yield 'XML stylesheet' => ['<?xml-stylesheet href="style.xsl"?>', false, true, null];
yield 'arbitrary XML processing instruction' => ['<?xml-custom value="x"?>', false, true, 1];
yield 'plain text' => ['plain text', false, true, null];
}

Expand Down
76 changes: 76 additions & 0 deletions tests/Rule/File/Psr1PhpTagsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Boundwize\StructArmed\Rule\Rules\File\PhpFileFinder;
use Boundwize\StructArmed\Rule\Rules\File\Psr1PhpTagsRule;
use Boundwize\StructArmed\Rule\RuleViolation;
use Boundwize\StructArmed\Util\InlineHtmlOpeningTagMatcher;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

Expand All @@ -31,6 +32,7 @@

#[CoversClass(PhpFileFinder::class)]
#[CoversClass(Psr1PhpTagsRule::class)]
#[CoversClass(InlineHtmlOpeningTagMatcher::class)]
final class Psr1PhpTagsRuleTest extends TestCase
{
public function testViolatesShortOpenTag(): void
Expand All @@ -51,6 +53,44 @@ public function testViolatesShortOpenTag(): void
}
}

public function testViolatesAssignmentUsingShortOpenTag(): void
{
$basePath = $this->makeTempDir();

try {
mkdir($basePath . '/src');
file_put_contents($basePath . '/src/Foo.php', '<? $value = 1; ?>');

$violations = (new Psr1PhpTagsRule(['src/']))->evaluateProjectAll($basePath, Architecture::define());

$this->assertCount(1, $violations);
$this->assertSame(1, $violations[0]->line);
} finally {
unlink($basePath . '/src/Foo.php');
rmdir($basePath . '/src');
rmdir($basePath);
}
}

public function testViolatesCommentPrefixedShortOpenTag(): void
{
$basePath = $this->makeTempDir();

try {
mkdir($basePath . '/src');
file_put_contents($basePath . '/src/Foo.php', "<? /* comment */ echo 'x'; ?>");

$violations = (new Psr1PhpTagsRule(['src/']))->evaluateProjectAll($basePath, Architecture::define());

$this->assertCount(1, $violations);
$this->assertSame(1, $violations[0]->line);
} finally {
unlink($basePath . '/src/Foo.php');
rmdir($basePath . '/src');
rmdir($basePath);
}
}

public function testPassesLongAndEchoTags(): void
{
$basePath = $this->makeTempDir();
Expand All @@ -69,6 +109,42 @@ public function testPassesLongAndEchoTags(): void
}
}

public function testPassesXmlProcessingInstructionsInInlineHtml(): void
{
$basePath = $this->makeTempDir();

try {
mkdir($basePath . '/src');
$contents = <<<'PHP'
<?php

header('Content-Type: application/xml');

?>
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="sitemap.xsl"?>
<urlset />
PHP;
file_put_contents($basePath . '/src/sitemap.php', $contents);

$psr1PhpTagsRule = new Psr1PhpTagsRule(['src/']);
$violations = $psr1PhpTagsRule->evaluateProjectAll($basePath, Architecture::define());

$this->assertSame([], $violations);
$this->assertFalse($psr1PhpTagsRule->fix(new RuleViolation(
message: 'File must use only valid PHP tags',
file: $basePath . '/src/sitemap.php',
line: 6,
className: '',
)));
$this->assertSame($contents, file_get_contents($basePath . '/src/sitemap.php'));
} finally {
unlink($basePath . '/src/sitemap.php');
rmdir($basePath . '/src');
rmdir($basePath);
}
}

public function testIgnoresPhpTagsInsideStrings(): void
{
$basePath = $this->makeTempDir();
Expand Down
Loading