Skip to content

Commit e9cc354

Browse files
committed
Use Option
1 parent d533126 commit e9cc354

File tree

10 files changed

+145
-121
lines changed

10 files changed

+145
-121
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"symfony/finder": "^6.1|^7.0",
2727
"symfony/console": "^6.1|^7.0",
2828
"symfony/event-dispatcher": "^6.1|^7.0",
29-
"webmozart/assert": "^1.11"
29+
"webmozart/assert": "^1.11",
30+
"texthtml/maybe": "^0.6.0"
3031
},
3132
"require-dev": {
3233
"phpunit/phpunit": "^10.0|^11.0|^12.0",

src/Application.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Symfony\Component\Console\Input;
88
use Symfony\Component\Console\Output;
99
use Symfony\Component\Console\SingleCommandApplication;
10+
use TH\Maybe\Option;
1011

1112
#[AsCommand("doctest")]
1213
final class Application extends SingleCommandApplication
@@ -61,22 +62,20 @@ protected function execute(Input\InputInterface $input, Output\OutputInterface $
6162
}
6263

6364
/**
64-
* @return list<string>|null
65+
* @return Option<array<string>>
6566
*/
66-
private function getLanguages(Input\InputInterface $input): ?array
67+
private function getLanguages(Input\InputInterface $input): Option
6768
{
6869
$languages = [];
6970

7071
foreach ($input->getOption("languages") as $lang) {
7172
if ($lang === '*') {
72-
$languages = null;
73-
74-
break;
73+
return Option\none();
7574
}
7675

7776
$languages[] = $lang;
7877
}
7978

80-
return $languages;
79+
return Option\some($languages);
8180
}
8281
}

src/Iterator/AllExamples.php

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44

55
use TH\DocTest\Example;
66
use TH\DocTest\Location;
7+
use TH\Maybe\Option;
78

89
final class AllExamples implements Examples
910
{
1011
/**
11-
* @param list<string>|null $acceptedLanguages Use empty string for unspecified language, and null for any languages
12+
* @param Option<array<string>> $languageFilter Use empty string for unspecified language
1213
*/
1314
public function __construct(
1415
private readonly Comments $comments,
15-
private readonly ?array $acceptedLanguages,
16+
private readonly Option $languageFilter,
1617
) {}
1718

1819
/**
@@ -27,15 +28,15 @@ public function getIterator(): \Traversable
2728

2829
/**
2930
* @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
31+
* @param Option<array<string>> $languageFilter Use empty string for unspecified language
3132
*/
3233
public static function fromPaths(
3334
array $paths,
34-
?array $acceptedLanguages,
35+
Option $languageFilter,
3536
): self {
3637
return new self(
3738
SourceComments::fromPaths($paths),
38-
$acceptedLanguages,
39+
$languageFilter,
3940
);
4041
}
4142

@@ -49,62 +50,66 @@ private function iterateComment(
4950
$lines = new \ArrayIterator(\explode(PHP_EOL, $comment));
5051
$index = 1;
5152

52-
while ($example = $this->nextExample($lines, $location, $index++)) {
53-
yield $example;
54-
}
53+
do {
54+
$example = $this->nextExample($lines, $location, $index++);
55+
56+
if ($example->isNone()) {
57+
return;
58+
}
59+
60+
yield $example->unwrap();
61+
} while (true);
5562
}
5663

5764
/**
5865
* @param \ArrayIterator<int,string> $lines
66+
* @return Option<Example>
5967
*/
6068
private function nextExample(
6169
\ArrayIterator $lines,
6270
Location $location,
6371
int $index,
64-
): ?Example {
65-
$codeblockStartedAt = $this->findFencedPHPCodeBlockStart($lines);
66-
67-
if ($codeblockStartedAt === null) {
68-
return null;
69-
}
70-
71-
return $this->readExample(
72-
$lines,
73-
$location->startingAt($codeblockStartedAt, $index),
74-
);
72+
): Option {
73+
return $this->findFencedPHPCodeBlockStart($lines)
74+
->andThen(fn ($codeblockStartedAt) => $this->readExample(
75+
$lines,
76+
$location->startingAt($codeblockStartedAt, $index),
77+
));
7578
}
7679

7780
/**
7881
* @param \ArrayIterator<int,string> $lines
82+
* @return Option<Example>
7983
*/
8084
private function readExample(
8185
\ArrayIterator $lines,
8286
Location $location,
83-
): ?Example {
87+
): Option {
8488
$buffer = [];
8589

8690
while ($lines->valid()) {
8791
$line = $lines->current();
8892
$lines->next();
8993

9094
if ($this->endOfAFencedCodeBlock($line)) {
91-
return new Example(
95+
return Option\some(new Example(
9296
\implode(PHP_EOL, $buffer),
9397
$location->ofLength($lines->key()),
94-
);
98+
));
9599
}
96100

97101
$buffer[] = \preg_replace("/^\s*\*( ?)/", "", $line);
98102
}
99103

100-
return null;
104+
return Option\none();
101105
}
102106

103107
/**
104108
* @param \ArrayIterator<int,string> $lines
105109
* phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
110+
* @return Option<int>
106111
*/
107-
private function findFencedPHPCodeBlockStart(\ArrayIterator $lines): ?int
112+
private function findFencedPHPCodeBlockStart(\ArrayIterator $lines): Option
108113
{
109114
$insideAFencedCodeBlock = false;
110115

@@ -119,43 +124,45 @@ private function findFencedPHPCodeBlockStart(\ArrayIterator $lines): ?int
119124
} else {
120125
$lang = $this->startOfAFencedCodeBlock($line);
121126

122-
if ($lang === false) {
123-
continue;
127+
if ($lang->mapOr($this->isAcceptedLanguage(...), default: false)) {
128+
return Option\some($lines->key());
124129
}
125130

126-
if ($this->isAcceptedLanguage($lang)) {
127-
return $lines->key();
131+
if ($lang->isNone()) {
132+
continue;
128133
}
129134

130135
$insideAFencedCodeBlock = true;
131136
}
132137
}
133138

134-
return null;
139+
return Option\none();
135140
}
136141

137142
private function isAcceptedLanguage(string $lang): bool
138143
{
139-
if ($this->acceptedLanguages === null) {
140-
return true;
141-
}
142-
143-
return \in_array(needle: $lang, haystack: $this->acceptedLanguages, strict: true);
144+
return $this->languageFilter->mapOr(
145+
callback: static fn (array $languages) => \in_array(needle: $lang, haystack: $languages, strict: true),
146+
default: true,
147+
);
144148
}
145149

146150
private function endOfAFencedCodeBlock(string $line): bool
147151
{
148152
return \ltrim($line) === "* ```";
149153
}
150154

151-
private function startOfAFencedCodeBlock(string $line): false|string
155+
/**
156+
* @return Option<string>
157+
*/
158+
private function startOfAFencedCodeBlock(string $line): Option
152159
{
153160
$line = \trim($line);
154161

155162
if (!\str_starts_with($line, "* ```")) {
156-
return false;
163+
return Option\none();
157164
}
158165

159-
return \substr($line, 5);
166+
return Option\some(\substr($line, 5));
160167
}
161168
}

src/Iterator/Files.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,7 @@ public function iteratePaths(): \Traversable
4545
*/
4646
private function iterate(string $pattern): \Traversable
4747
{
48-
$paths = \glob($pattern);
49-
50-
if ($paths === false) {
51-
return;
52-
}
53-
54-
foreach ($paths as $path) {
48+
foreach (\glob($pattern) ?? [] as $path) {
5549
yield from $this->iteratePath($path);
5650
}
5751
}

src/Iterator/FilteredExamples.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace TH\DocTest\Iterator;
44

55
use TH\DocTest\Example;
6+
use TH\Maybe\Option;
67

78
final class FilteredExamples implements Examples
89
{
@@ -30,15 +31,15 @@ public static function filter(Examples $examples, string $filter): self
3031

3132
/**
3233
* @param array<string> $paths paths to files and folder to look for PHP comments code examples in
33-
* @param list<string>|null $acceptedLanguages Use empty string for unspecified language, and null for any languages
34+
* @param Option<array<string>> $languageFilter Use empty string for unspecified language
3435
*/
3536
public static function fromPaths(
3637
array $paths,
3738
string $filter,
38-
?array $acceptedLanguages,
39+
Option $languageFilter,
3940
): self {
4041
return self::filter(
41-
AllExamples::fromPaths($paths, $acceptedLanguages),
42+
AllExamples::fromPaths($paths, $languageFilter),
4243
$filter,
4344
);
4445
}

src/Location.php

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
namespace TH\DocTest;
44

5+
use TH\Maybe\Option;
6+
57
final class Location implements \Stringable
68
{
79
/**
810
* @param \ReflectionClass<*>|\ReflectionMethod|\ReflectionFunction $source
11+
* @param Option<string> $path,
12+
* @param Option<int> $startLine,
13+
* @param Option<int> $endLine,
914
*/
1015
public function __construct(
1116
public readonly \ReflectionClass|\ReflectionMethod|\ReflectionFunction $source,
1217
public readonly string $name,
13-
public readonly ?string $path,
14-
public readonly ?int $startLine,
15-
public readonly ?int $endLine,
18+
public readonly Option $path,
19+
public readonly Option $startLine,
20+
public readonly Option $endLine,
1621
public readonly int $index,
1722
) {}
1823

@@ -22,8 +27,8 @@ public function startingAt(int $offset, int $index): Location
2227
$this->source,
2328
$this->name,
2429
$this->path,
25-
$this->startLine !== null ? $this->startLine + $offset : null,
26-
null,
30+
$this->startLine->map(static fn (int $startLine) => $startLine + $offset),
31+
Option\none(),
2732
$index,
2833
);
2934
}
@@ -35,7 +40,7 @@ public function ofLength(int $length): Location
3540
$this->name,
3641
$this->path,
3742
$this->startLine,
38-
$this->startLine !== null ? $this->startLine + $length : null,
43+
$this->startLine->map(static fn (int $startLine) => $startLine + $length),
3944
$this->index,
4045
);
4146
}
@@ -53,49 +58,55 @@ public static function fromReflection(
5358
$name = "{$source->getDeclaringClass()->getName()}::$name(…)";
5459
}
5560

56-
$startLine = $source->getStartLine();
57-
58-
if ($startLine !== false) {
59-
$endLine = $startLine;
60-
$startLine -= \substr_count($comment, \PHP_EOL);
61-
} else {
62-
$endLine = $startLine = null;
63-
}
61+
$endLine = Option\fromValue($source->getStartLine(), noneValue: false);
62+
$startLine = $endLine->map(static fn (int $endLine) => $endLine - \substr_count($comment, \PHP_EOL));
6463

6564
return new self(
6665
$source,
6766
$name,
68-
self::makePathRelative($source->getFileName()),
67+
self::makePathRelative(Option\fromValue($source->getFileName(), noneValue: false)),
6968
$startLine,
7069
$endLine,
7170
1,
7271
);
7372
}
7473

75-
private static function makePathRelative(string|false $path): ?string
74+
/**
75+
* @param Option<string> $path
76+
* @return Option<string>
77+
*/
78+
private static function makePathRelative(Option $path): Option
7679
{
7780
static $stripSrcDirPattern;
7881

79-
if ($path === false) {
80-
return null;
81-
}
82-
8382
$stripSrcDirPattern ??=
84-
"/^" .
85-
\preg_quote(
86-
str: ($cwd = \getcwd()) !== false
87-
? $cwd
88-
: throw new \RuntimeException("getwd failed"),
89-
delimiter: "/",
90-
) .
91-
"(\/*)/";
83+
"/^" .
84+
\preg_quote(
85+
str: ($cwd = \getcwd()) !== false
86+
? $cwd
87+
: throw new \RuntimeException("getwd failed"),
88+
delimiter: "/",
89+
) .
90+
"(\/*)/";
9291

93-
return \preg_replace($stripSrcDirPattern, "", $path) ??
94-
throw new \RuntimeException("Making path relative failed for : $path");
92+
return $path->map(
93+
static fn (string $path) => \preg_replace($stripSrcDirPattern, "", $path) ??
94+
throw new \RuntimeException("Making path relative failed for : $path"),
95+
);
9596
}
9697

9798
public function __toString(): string
9899
{
99-
return "{$this->name}#{$this->index} ({$this->path}:{$this->startLine})";
100+
$suffix = $this->path
101+
->map(
102+
fn (string $path) => $this->startLine->mapOr(
103+
static fn (int $startLine) => "$path:$startLine",
104+
$path,
105+
),
106+
)
107+
->map(static fn (string $suffix) => " ($suffix)")
108+
->unwrapOr("");
109+
110+
return "{$this->name}#{$this->index}$suffix";
100111
}
101112
}

0 commit comments

Comments
 (0)