Skip to content

Commit d533126

Browse files
committed
test php fenced block only by default
1 parent d7ac2a1 commit d533126

40 files changed

+416
-91
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/composer.lock
12
/vendor/
23
/phpstan.neon
34
/.phpunit.result.cache

.phpactor.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "/phpactor.schema.json",
3+
"language_server_phpstan.enabled": true,
4+
"php_code_sniffer.enabled": true,
5+
"prophecy.enabled": false
6+
}

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ example will be marked as failed.
1515
/**
1616
* Compute the factorial of a non-negative $n
1717
*
18-
* ```
18+
* ```php
1919
* assert(factorial(0) === 1);
2020
* assert(factorial(5) === 120);
2121
* ```
@@ -105,4 +105,3 @@ $ ./bin/doctest examples/factorial.php
105105
# TODO
106106

107107
* Junit output
108-
* Find PHP examples in markdown

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
},
3131
"require-dev": {
3232
"phpunit/phpunit": "^10.0|^11.0|^12.0",
33-
"phpspec/prophecy-phpunit": "^2.0",
3433
"squizlabs/php_codesniffer": "^3.6",
3534
"slevomat/coding-standard": "^8.0",
3635
"phpstan/phpstan": "^2.0",

examples/Ratio.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
<?php
22

33
/**
4-
* ```
4+
* ```php
55
* $ratio = new Ratio(0.3);
66
* echo $ratio->formatPercentage(); // @prints 30%
77
* ```
88
*
9-
* ```
9+
* ```php
1010
* $ratio = new Ratio(0.9);
1111
* $ratio->value = 0.1; // @throws Error Cannot modify readonly property Ratio::$value
1212
* ```
1313
*/
1414
final class Ratio
1515
{
1616
/**
17-
* ```
17+
* ```php
1818
* new Ratio(log(0)); // @throws InvalidArgumentException Ratio only accepts finite values. Got -INF
1919
* ```
2020
*/
@@ -27,7 +27,7 @@ public function __construct(
2727
}
2828

2929
/**
30-
* ```
30+
* ```php
3131
* assert(Ratio::fromPercentage(80)->value === 0.8);
3232
* ```
3333
*/
@@ -37,7 +37,7 @@ public static function fromPercentage(float $percentage): Ratio
3737
}
3838

3939
/**
40-
* ```
40+
* ```php
4141
* $ratio = new Ratio(0.167);
4242
* self::assertEq($ratio->formatPercentage(2), "16.70%");
4343
* ```

examples/factorial.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,43 @@
33
/**
44
* Compute the factorial of a non-negative $n
55
*
6-
* ```
6+
* ```php
77
* assert(factorial(0) === 1);
88
* assert(factorial(1) === 1);
99
* assert(factorial(2) === 2);
1010
* assert(factorial(3) === 6);
1111
* ```
1212
*
13-
* ```
13+
* ```php
1414
* // You can use `self::assert*` to call `Webmozart\Assert\Assert::*` assertion helpers
1515
* self::assertEq(factorial(4), 24);
1616
* self::assertEq(factorial(5), 120);
1717
* ```
1818
*
19-
* ```
19+
* ```php
2020
* // This example will fail because of unexpected output
2121
* echo factorial(6);
2222
* ```
2323
*
24-
* ```
24+
* ```php
2525
* // You can check for expected output with one or more `@prints [text]` inline comments
2626
* echo factorial(6); // @prints 720
2727
* ```
2828
*
29-
* ```
29+
* ```php
3030
* // This example will fail with InvalidArgumentException unexpected negative integer: -10
3131
* factorial(-10);
3232
* ```
3333
*
34-
* ```
34+
* ```php
3535
* // You can check for Exception with a `@throws [Exception class] [Exception message]` inline comment
3636
* factorial(-10); // @throws InvalidArgumentException unexpected negative integer: -10
3737
*
3838
* // Everything after the exception will be ignored. So this won't fail:
3939
* assert(1 === 2);
4040
* ```
4141
*
42-
* ```
42+
* ```php
4343
* // This example will fail with the message : "Expected a value equal to 3628890. Got: 3628800"
4444
* self::assertEq(factorial(10), 3628890);
4545
* ```

phpcs.xml.dist

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55

66
<file>src</file>
77
<file>tests</file>
8-
<exclude-pattern>tests/data/examples/</exclude-pattern>
9-
10-
<exclude-pattern>tests/examples.php</exclude-pattern>
8+
<exclude-pattern>tests/data/</exclude-pattern>
119

1210
<rule ref="PSR12">
1311
<exclude name="PSR12.Files.FileHeader" />

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ parameters:
1212
uncheckedExceptionClasses:
1313
- RuntimeException
1414
excludePaths:
15-
- tests/data/examples
15+
- tests/data/code-blocs/

src/Application.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#[AsCommand("doctest")]
1212
final class Application extends SingleCommandApplication
1313
{
14+
// phpcs:disable SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
1415
protected function configure(): void
1516
{
1617
$this
@@ -27,8 +28,16 @@ protected function configure(): void
2728
"filter",
2829
"f",
2930
Input\InputOption::VALUE_REQUIRED,
30-
"Ony run code blocks whose names matche the filter",
31+
"Ony run code blocks whose names match the filter",
3132
default: "",
33+
)
34+
->addOption(
35+
"languages",
36+
"l",
37+
Input\InputOption::VALUE_REQUIRED | Input\InputOption::VALUE_IS_ARRAY,
38+
"Ony run code blocks whose language match." . PHP_EOL
39+
. "(\"*\" to match any language, \"\" to match unspecified language)",
40+
default: ["php"],
3241
);
3342
}
3443

@@ -37,6 +46,7 @@ protected function execute(Input\InputInterface $input, Output\OutputInterface $
3746
$testSuite = TestSuite::fromPaths(
3847
$input->getArgument("paths"),
3948
$input->getOption("filter"),
49+
$this->getLanguages($input),
4050
);
4151

4252
$testSuite->addSubscriber(new Subscriber\ProgressBar($input, $output));
@@ -49,4 +59,24 @@ protected function execute(Input\InputInterface $input, Output\OutputInterface $
4959
? Command::SUCCESS
5060
: Command::FAILURE;
5161
}
62+
63+
/**
64+
* @return list<string>|null
65+
*/
66+
private function getLanguages(Input\InputInterface $input): ?array
67+
{
68+
$languages = [];
69+
70+
foreach ($input->getOption("languages") as $lang) {
71+
if ($lang === '*') {
72+
$languages = null;
73+
74+
break;
75+
}
76+
77+
$languages[] = $lang;
78+
}
79+
80+
return $languages;
81+
}
5282
}

src/Iterator/AllExamples.php

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77

88
final class AllExamples implements Examples
99
{
10+
/**
11+
* @param list<string>|null $acceptedLanguages Use empty string for unspecified language, and null for any languages
12+
*/
1013
public function __construct(
11-
private Comments $comments,
12-
) {
13-
}
14+
private readonly Comments $comments,
15+
private readonly ?array $acceptedLanguages,
16+
) {}
1417

1518
/**
1619
* @return \Traversable<Example>
@@ -24,17 +27,25 @@ public function getIterator(): \Traversable
2427

2528
/**
2629
* @param array<string> $paths paths to files and folder to look for PHP comments code examples in
30+
* @param list<string>|null $acceptedLanguages Use empty string for unspecified language, and null for any languages
2731
*/
28-
public static function fromPaths(array $paths): self
29-
{
30-
return new self(Comments::fromPaths($paths));
32+
public static function fromPaths(
33+
array $paths,
34+
?array $acceptedLanguages,
35+
): self {
36+
return new self(
37+
SourceComments::fromPaths($paths),
38+
$acceptedLanguages,
39+
);
3140
}
3241

3342
/**
3443
* @return \Traversable<Example>
3544
*/
36-
private function iterateComment(string $comment, Location $location): \Traversable
37-
{
45+
private function iterateComment(
46+
string $comment,
47+
Location $location,
48+
): \Traversable {
3849
$lines = new \ArrayIterator(\explode(PHP_EOL, $comment));
3950
$index = 1;
4051

@@ -46,30 +57,41 @@ private function iterateComment(string $comment, Location $location): \Traversab
4657
/**
4758
* @param \ArrayIterator<int,string> $lines
4859
*/
49-
private function nextExample(\ArrayIterator $lines, Location $location, int $index): ?Example
50-
{
51-
$codeblockStartedAt = $this->findFencedCodeBlockStart($lines);
60+
private function nextExample(
61+
\ArrayIterator $lines,
62+
Location $location,
63+
int $index,
64+
): ?Example {
65+
$codeblockStartedAt = $this->findFencedPHPCodeBlockStart($lines);
5266

5367
if ($codeblockStartedAt === null) {
5468
return null;
5569
}
5670

57-
return $this->readExample($lines, $location->startingAt($codeblockStartedAt, $index));
71+
return $this->readExample(
72+
$lines,
73+
$location->startingAt($codeblockStartedAt, $index),
74+
);
5875
}
5976

6077
/**
6178
* @param \ArrayIterator<int,string> $lines
6279
*/
63-
private function readExample(\ArrayIterator $lines, Location $location): ?Example
64-
{
80+
private function readExample(
81+
\ArrayIterator $lines,
82+
Location $location,
83+
): ?Example {
6584
$buffer = [];
6685

6786
while ($lines->valid()) {
6887
$line = $lines->current();
6988
$lines->next();
7089

71-
if ($this->endOfAPHPFencedCodeBlock($line)) {
72-
return new Example(\implode(PHP_EOL, $buffer), $location->ofLength($lines->key()));
90+
if ($this->endOfAFencedCodeBlock($line)) {
91+
return new Example(
92+
\implode(PHP_EOL, $buffer),
93+
$location->ofLength($lines->key()),
94+
);
7395
}
7496

7597
$buffer[] = \preg_replace("/^\s*\*( ?)/", "", $line);
@@ -80,28 +102,60 @@ private function readExample(\ArrayIterator $lines, Location $location): ?Exampl
80102

81103
/**
82104
* @param \ArrayIterator<int,string> $lines
105+
* phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
83106
*/
84-
private function findFencedCodeBlockStart(\ArrayIterator $lines): ?int
107+
private function findFencedPHPCodeBlockStart(\ArrayIterator $lines): ?int
85108
{
109+
$insideAFencedCodeBlock = false;
110+
86111
while ($lines->valid()) {
87112
$line = $lines->current();
88113
$lines->next();
89114

90-
if ($this->startOfAPHPFencedCodeBlock($line)) {
91-
return $lines->key();
115+
if ($insideAFencedCodeBlock) {
116+
if ($this->endOfAFencedCodeBlock($line)) {
117+
$insideAFencedCodeBlock = false;
118+
}
119+
} else {
120+
$lang = $this->startOfAFencedCodeBlock($line);
121+
122+
if ($lang === false) {
123+
continue;
124+
}
125+
126+
if ($this->isAcceptedLanguage($lang)) {
127+
return $lines->key();
128+
}
129+
130+
$insideAFencedCodeBlock = true;
92131
}
93132
}
94133

95134
return null;
96135
}
97136

98-
private function endOfAPHPFencedCodeBlock(string $line): bool
137+
private function isAcceptedLanguage(string $lang): bool
138+
{
139+
if ($this->acceptedLanguages === null) {
140+
return true;
141+
}
142+
143+
return \in_array(needle: $lang, haystack: $this->acceptedLanguages, strict: true);
144+
}
145+
146+
private function endOfAFencedCodeBlock(string $line): bool
99147
{
100148
return \ltrim($line) === "* ```";
101149
}
102150

103-
private function startOfAPHPFencedCodeBlock(string $line): bool
151+
private function startOfAFencedCodeBlock(string $line): false|string
104152
{
105-
return \in_array(\ltrim($line), ["* ```", "* ```php"], strict: true);
153+
$line = \trim($line);
154+
155+
if (!\str_starts_with($line, "* ```")) {
156+
return false;
157+
}
158+
159+
return \substr($line, 5);
106160
}
107161
}

0 commit comments

Comments
 (0)