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
91 changes: 0 additions & 91 deletions src/Sniff/AbstractSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@

abstract class AbstractSniff implements SniffInterface
{
private const array NON_ELEMENT_DELIMITERS = [
'<!--' => '-->',
'<![CDATA[' => ']]>',
'<?' => '?>',
];

protected Severity $severity = Severity::ERROR;

/** @var array<string, string> */
Expand Down Expand Up @@ -91,89 +85,4 @@ protected function createViolation(string $filePath, string $message, array $aff
severity: $this->severity,
);
}

protected function maskNonElementMarkup(string $source): string
{
$masked = $source;
$offset = 0;

while (false !== $start = strpos($source, '<', $offset)) {
$endOffset = $this->nonElementMarkupEndOffset($source, $start);

if ($endOffset === null) {
$offset = $start + 1;
continue;
}

for ($i = $start; $i < $endOffset; $i++) {
$masked[$i] = ' ';
}

$offset = $endOffset;
}

return $masked;
}

private function nonElementMarkupEndOffset(string $source, int $start): ?int
{
foreach (self::NON_ELEMENT_DELIMITERS as $opening => $closing) {
if (substr_compare($source, $opening, $start, strlen($opening)) === 0) {
return $this->offsetAfterDelimiter($source, $closing, $start);
}
}

if (substr_compare($source, '<!', $start, 2) === 0) {
return $this->declarationEndOffset($source, $start);
}

return null;
}

private function offsetAfterDelimiter(string $source, string $delimiter, int $offset): int
{
$end = strpos($source, $delimiter, $offset);

return $end === false ? strlen($source) : $end + strlen($delimiter);
}

private function declarationEndOffset(string $source, int $offset): int
{
$length = strlen($source);
$quote = null;
$bracketDepth = 0;

for ($i = $offset; $i < $length; $i++) {
$character = $source[$i];

if ($quote !== null) {
if ($character === $quote) {
$quote = null;
}

continue;
}

if ($character === '"' || $character === "'") {
$quote = $character;
continue;
}

if ($character === '[') {
$bracketDepth++;
continue;
}

if ($character === ']') {
$bracketDepth--;
continue;
}

if ($character === '>' && $bracketDepth === 0) {
return $i + 1;
}
}

return $length;
}
}
2 changes: 1 addition & 1 deletion src/Sniff/AttributeOrderSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function process(\DOMDocument $document, File $file): array
// Match ONLY opening tags (skip closing, comments, xml decl)
preg_match_all(
self::OPENING_TAG_PATTERN,
$this->maskNonElementMarkup($file->content),
$file->contentWithNonElementMarkupMasked(),
$matches,
PREG_OFFSET_CAPTURE,
);
Expand Down
2 changes: 1 addition & 1 deletion src/Sniff/ExceptionNameSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private function sourceMatches(File $file): array
{
preg_match_all(
self::CLASSNAME_PATTERN,
$this->maskNonElementMarkup($file->content),
$file->contentWithNonElementMarkupMasked(),
$matches,
PREG_OFFSET_CAPTURE,
);
Expand Down
2 changes: 1 addition & 1 deletion src/Sniff/SimparaSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private function sourceMatches(File $file): array
{
preg_match_all(
self::PARA_TAG_PATTERN,
$this->maskNonElementMarkup($file->content),
$file->contentWithNonElementMarkupMasked(),
$matches,
PREG_OFFSET_CAPTURE,
);
Expand Down
97 changes: 97 additions & 0 deletions src/Source/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@

final class File
{
private const array NON_ELEMENT_DELIMITERS = [
'<!--' => '-->',
'<![CDATA[' => ']]>',
'<?' => '?>',
];

/** @var non-empty-list<int>|null */
private ?array $lineBeginOffsets = null;

private ?string $maskedContent = null;

public function __construct(
public readonly string $path,
public readonly string $content,
Expand Down Expand Up @@ -48,6 +56,33 @@ public function withContent(string $content): self
: new self($this->path, $content);
}

public function contentWithNonElementMarkupMasked(): string
{
if ($this->maskedContent !== null) {
return $this->maskedContent;
}

$masked = $this->content;
$offset = 0;

while (false !== $start = strpos($this->content, '<', $offset)) {
$endOffset = $this->nonElementMarkupEndOffset($start);

if ($endOffset === null) {
$offset = $start + 1;
continue;
}

for ($i = $start; $i < $endOffset; $i++) {
$masked[$i] = ' ';
}

$offset = $endOffset;
}

return $this->maskedContent = $masked;
}

/** @return non-empty-list<int> */
private function lineBeginOffsets(): array
{
Expand Down Expand Up @@ -108,6 +143,68 @@ private function lineIndexAtOffset(int $offset, array $lineBeginOffsets): int
return $low;
}

private function nonElementMarkupEndOffset(int $start): ?int
{
foreach (self::NON_ELEMENT_DELIMITERS as $opening => $closing) {
if (substr_compare($this->content, $opening, $start, strlen($opening)) === 0) {
return $this->offsetAfterDelimiter($closing, $start);
}
}

if (substr_compare($this->content, '<!', $start, 2) === 0) {
return $this->declarationEndOffset($start);
}

return null;
}

private function offsetAfterDelimiter(string $delimiter, int $offset): int
{
$end = strpos($this->content, $delimiter, $offset);

return $end === false ? strlen($this->content) : $end + strlen($delimiter);
}

private function declarationEndOffset(int $offset): int
{
$length = strlen($this->content);
$quote = null;
$bracketDepth = 0;

for ($i = $offset; $i < $length; $i++) {
$character = $this->content[$i];

if ($quote !== null) {
if ($character === $quote) {
$quote = null;
}

continue;
}

if ($character === '"' || $character === "'") {
$quote = $character;
continue;
}

if ($character === '[') {
$bracketDepth++;
continue;
}

if ($character === ']') {
$bracketDepth--;
continue;
}

if ($character === '>' && $bracketDepth === 0) {
return $i + 1;
}
}

return $length;
}

private function createLineAtOffset(int $lineNumber, int $lineBeginOffset): Line
{
$sourceLength = strlen($this->content);
Expand Down
5 changes: 0 additions & 5 deletions tests/Support/Sniff/ExposedAbstractSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,4 @@ public function exposeSeverity(): Severity
{
return $this->severity;
}

public function exposeMaskNonElementMarkup(string $source): string
{
return $this->maskNonElementMarkup($source);
}
}
9 changes: 0 additions & 9 deletions tests/Unit/Sniff/AbstractSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,4 @@ public function itRejectsInvalidSeverityProperties(): void

new ExposedAbstractSniff()->setProperty('severity', 'invalid');
}

#[Test]
public function itMasksAnUnterminatedDeclarationThroughTheEndOfTheSource(): void
{
$source = '<!DOCTYPE root [';
$masked = new ExposedAbstractSniff()->exposeMaskNonElementMarkup($source);

self::assertSame(str_repeat(' ', strlen($source)), $masked);
}
}
32 changes: 32 additions & 0 deletions tests/Unit/Source/FileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ public function itReusesTheCurrentRevisionForUnchangedContent(): void
self::assertSame($file, $file->withContent('<root/>'));
}

#[Test]
public function itMasksNonElementMarkupWithoutChangingSourceOffsets(): void
{
$content = <<<'XML'
<!DOCTYPE root [<!ENTITY sample "<para>Declared</para>">]>
<root>
<!-- <para>Commented</para> -->
<![CDATA[<para>CDATA</para>]]>
<?sample <para>Instruction</para>?>
<para>Source</para>
</root>
XML;

$masked = new File('file.xml', $content)->contentWithNonElementMarkupMasked();

self::assertSame(strlen($content), strlen($masked));
self::assertSame(strpos($content, '<para>Source</para>'), strpos($masked, '<para>Source</para>'));
self::assertStringNotContainsString('<para>Declared</para>', $masked);
self::assertStringNotContainsString('<para>Commented</para>', $masked);
self::assertStringNotContainsString('<para>CDATA</para>', $masked);
self::assertStringNotContainsString('<para>Instruction</para>', $masked);
}

#[Test]
public function itMasksAnUnterminatedDeclarationThroughTheEndOfTheSource(): void
{
$source = '<!DOCTYPE root [';
$masked = new File('file.xml', $source)->contentWithNonElementMarkupMasked();

self::assertSame(str_repeat(' ', strlen($source)), $masked);
}

#[Test]
public function itRejectsOffsetsOutsideTheSource(): void
{
Expand Down