Skip to content

Commit aaf2bc0

Browse files
committed
perf: cached masked source content
1 parent 6d19229 commit aaf2bc0

8 files changed

Lines changed: 132 additions & 108 deletions

File tree

src/Sniff/AbstractSniff.php

Lines changed: 0 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@
1212

1313
abstract class AbstractSniff implements SniffInterface
1414
{
15-
private const array NON_ELEMENT_DELIMITERS = [
16-
'<!--' => '-->',
17-
'<![CDATA[' => ']]>',
18-
'<?' => '?>',
19-
];
20-
2115
protected Severity $severity = Severity::ERROR;
2216

2317
/** @var array<string, string> */
@@ -91,89 +85,4 @@ protected function createViolation(string $filePath, string $message, array $aff
9185
severity: $this->severity,
9286
);
9387
}
94-
95-
protected function maskNonElementMarkup(string $source): string
96-
{
97-
$masked = $source;
98-
$offset = 0;
99-
100-
while (false !== $start = strpos($source, '<', $offset)) {
101-
$endOffset = $this->nonElementMarkupEndOffset($source, $start);
102-
103-
if ($endOffset === null) {
104-
$offset = $start + 1;
105-
continue;
106-
}
107-
108-
for ($i = $start; $i < $endOffset; $i++) {
109-
$masked[$i] = ' ';
110-
}
111-
112-
$offset = $endOffset;
113-
}
114-
115-
return $masked;
116-
}
117-
118-
private function nonElementMarkupEndOffset(string $source, int $start): ?int
119-
{
120-
foreach (self::NON_ELEMENT_DELIMITERS as $opening => $closing) {
121-
if (substr_compare($source, $opening, $start, strlen($opening)) === 0) {
122-
return $this->offsetAfterDelimiter($source, $closing, $start);
123-
}
124-
}
125-
126-
if (substr_compare($source, '<!', $start, 2) === 0) {
127-
return $this->declarationEndOffset($source, $start);
128-
}
129-
130-
return null;
131-
}
132-
133-
private function offsetAfterDelimiter(string $source, string $delimiter, int $offset): int
134-
{
135-
$end = strpos($source, $delimiter, $offset);
136-
137-
return $end === false ? strlen($source) : $end + strlen($delimiter);
138-
}
139-
140-
private function declarationEndOffset(string $source, int $offset): int
141-
{
142-
$length = strlen($source);
143-
$quote = null;
144-
$bracketDepth = 0;
145-
146-
for ($i = $offset; $i < $length; $i++) {
147-
$character = $source[$i];
148-
149-
if ($quote !== null) {
150-
if ($character === $quote) {
151-
$quote = null;
152-
}
153-
154-
continue;
155-
}
156-
157-
if ($character === '"' || $character === "'") {
158-
$quote = $character;
159-
continue;
160-
}
161-
162-
if ($character === '[') {
163-
$bracketDepth++;
164-
continue;
165-
}
166-
167-
if ($character === ']') {
168-
$bracketDepth--;
169-
continue;
170-
}
171-
172-
if ($character === '>' && $bracketDepth === 0) {
173-
return $i + 1;
174-
}
175-
}
176-
177-
return $length;
178-
}
17988
}

src/Sniff/AttributeOrderSniff.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function process(\DOMDocument $document, File $file): array
4343
// Match ONLY opening tags (skip closing, comments, xml decl)
4444
preg_match_all(
4545
self::OPENING_TAG_PATTERN,
46-
$this->maskNonElementMarkup($file->content),
46+
$file->contentWithNonElementMarkupMasked(),
4747
$matches,
4848
PREG_OFFSET_CAPTURE,
4949
);

src/Sniff/ExceptionNameSniff.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private function sourceMatches(File $file): array
121121
{
122122
preg_match_all(
123123
self::CLASSNAME_PATTERN,
124-
$this->maskNonElementMarkup($file->content),
124+
$file->contentWithNonElementMarkupMasked(),
125125
$matches,
126126
PREG_OFFSET_CAPTURE,
127127
);

src/Sniff/SimparaSniff.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ private function sourceMatches(File $file): array
219219
{
220220
preg_match_all(
221221
self::PARA_TAG_PATTERN,
222-
$this->maskNonElementMarkup($file->content),
222+
$file->contentWithNonElementMarkupMasked(),
223223
$matches,
224224
PREG_OFFSET_CAPTURE,
225225
);

src/Source/File.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@
66

77
final class File
88
{
9+
private const array NON_ELEMENT_DELIMITERS = [
10+
'<!--' => '-->',
11+
'<![CDATA[' => ']]>',
12+
'<?' => '?>',
13+
];
14+
915
/** @var non-empty-list<int>|null */
1016
private ?array $lineBeginOffsets = null;
1117

18+
private ?string $maskedContent = null;
19+
1220
public function __construct(
1321
public readonly string $path,
1422
public readonly string $content,
@@ -48,6 +56,33 @@ public function withContent(string $content): self
4856
: new self($this->path, $content);
4957
}
5058

59+
public function contentWithNonElementMarkupMasked(): string
60+
{
61+
if ($this->maskedContent !== null) {
62+
return $this->maskedContent;
63+
}
64+
65+
$masked = $this->content;
66+
$offset = 0;
67+
68+
while (false !== $start = strpos($this->content, '<', $offset)) {
69+
$endOffset = $this->nonElementMarkupEndOffset($start);
70+
71+
if ($endOffset === null) {
72+
$offset = $start + 1;
73+
continue;
74+
}
75+
76+
for ($i = $start; $i < $endOffset; $i++) {
77+
$masked[$i] = ' ';
78+
}
79+
80+
$offset = $endOffset;
81+
}
82+
83+
return $this->maskedContent = $masked;
84+
}
85+
5186
/** @return non-empty-list<int> */
5287
private function lineBeginOffsets(): array
5388
{
@@ -108,6 +143,68 @@ private function lineIndexAtOffset(int $offset, array $lineBeginOffsets): int
108143
return $low;
109144
}
110145

146+
private function nonElementMarkupEndOffset(int $start): ?int
147+
{
148+
foreach (self::NON_ELEMENT_DELIMITERS as $opening => $closing) {
149+
if (substr_compare($this->content, $opening, $start, strlen($opening)) === 0) {
150+
return $this->offsetAfterDelimiter($closing, $start);
151+
}
152+
}
153+
154+
if (substr_compare($this->content, '<!', $start, 2) === 0) {
155+
return $this->declarationEndOffset($start);
156+
}
157+
158+
return null;
159+
}
160+
161+
private function offsetAfterDelimiter(string $delimiter, int $offset): int
162+
{
163+
$end = strpos($this->content, $delimiter, $offset);
164+
165+
return $end === false ? strlen($this->content) : $end + strlen($delimiter);
166+
}
167+
168+
private function declarationEndOffset(int $offset): int
169+
{
170+
$length = strlen($this->content);
171+
$quote = null;
172+
$bracketDepth = 0;
173+
174+
for ($i = $offset; $i < $length; $i++) {
175+
$character = $this->content[$i];
176+
177+
if ($quote !== null) {
178+
if ($character === $quote) {
179+
$quote = null;
180+
}
181+
182+
continue;
183+
}
184+
185+
if ($character === '"' || $character === "'") {
186+
$quote = $character;
187+
continue;
188+
}
189+
190+
if ($character === '[') {
191+
$bracketDepth++;
192+
continue;
193+
}
194+
195+
if ($character === ']') {
196+
$bracketDepth--;
197+
continue;
198+
}
199+
200+
if ($character === '>' && $bracketDepth === 0) {
201+
return $i + 1;
202+
}
203+
}
204+
205+
return $length;
206+
}
207+
111208
private function createLineAtOffset(int $lineNumber, int $lineBeginOffset): Line
112209
{
113210
$sourceLength = strlen($this->content);

tests/Support/Sniff/ExposedAbstractSniff.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,4 @@ public function exposeSeverity(): Severity
2929
{
3030
return $this->severity;
3131
}
32-
33-
public function exposeMaskNonElementMarkup(string $source): string
34-
{
35-
return $this->maskNonElementMarkup($source);
36-
}
3732
}

tests/Unit/Sniff/AbstractSniffTest.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,4 @@ public function itRejectsInvalidSeverityProperties(): void
5353

5454
new ExposedAbstractSniff()->setProperty('severity', 'invalid');
5555
}
56-
57-
#[Test]
58-
public function itMasksAnUnterminatedDeclarationThroughTheEndOfTheSource(): void
59-
{
60-
$source = '<!DOCTYPE root [';
61-
$masked = new ExposedAbstractSniff()->exposeMaskNonElementMarkup($source);
62-
63-
self::assertSame(str_repeat(' ', strlen($source)), $masked);
64-
}
6556
}

tests/Unit/Source/FileTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,38 @@ public function itReusesTheCurrentRevisionForUnchangedContent(): void
8585
self::assertSame($file, $file->withContent('<root/>'));
8686
}
8787

88+
#[Test]
89+
public function itMasksNonElementMarkupWithoutChangingSourceOffsets(): void
90+
{
91+
$content = <<<'XML'
92+
<!DOCTYPE root [<!ENTITY sample "<para>Declared</para>">]>
93+
<root>
94+
<!-- <para>Commented</para> -->
95+
<![CDATA[<para>CDATA</para>]]>
96+
<?sample <para>Instruction</para>?>
97+
<para>Source</para>
98+
</root>
99+
XML;
100+
101+
$masked = new File('file.xml', $content)->contentWithNonElementMarkupMasked();
102+
103+
self::assertSame(strlen($content), strlen($masked));
104+
self::assertSame(strpos($content, '<para>Source</para>'), strpos($masked, '<para>Source</para>'));
105+
self::assertStringNotContainsString('<para>Declared</para>', $masked);
106+
self::assertStringNotContainsString('<para>Commented</para>', $masked);
107+
self::assertStringNotContainsString('<para>CDATA</para>', $masked);
108+
self::assertStringNotContainsString('<para>Instruction</para>', $masked);
109+
}
110+
111+
#[Test]
112+
public function itMasksAnUnterminatedDeclarationThroughTheEndOfTheSource(): void
113+
{
114+
$source = '<!DOCTYPE root [';
115+
$masked = new File('file.xml', $source)->contentWithNonElementMarkupMasked();
116+
117+
self::assertSame(str_repeat(' ', strlen($source)), $masked);
118+
}
119+
88120
#[Test]
89121
public function itRejectsOffsetsOutsideTheSource(): void
90122
{

0 commit comments

Comments
 (0)