Skip to content
This repository was archived by the owner on Mar 6, 2022. It is now read-only.

Commit c06ba79

Browse files
authored
Merge pull request #1 from phpactor/multiple-segments
Multiple segments
2 parents 9a8fb91 + 4bc1029 commit c06ba79

File tree

8 files changed

+341
-169
lines changed

8 files changed

+341
-169
lines changed

.travis.yml

-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
language: php
22

33
php:
4-
- 7.1
5-
- 7.2
64
- 7.3
75
- 7.4
86

README.md

+29-7
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@ Path Finder
33

44
[![Build Status](https://travis-ci.org/phpactor/path-finder.svg?branch=master)](https://travis-ci.org/phpactor/path-finder)
55

6-
Library to infer paths from a given path where paths share the same "kernel"
7-
(common section of the path).
6+
Library to infer paths from a given path where paths share path segments.
87

9-
For example, infer unit test path from the source file, from a unit test to
10-
a bechmark, from the benchmark to the source file etc.
8+
For example, infer test paths for a given source file and vice-versa.
119

1210
Usage
1311
-----
1412

1513
Path finder accepts a hash map of destinations and their schemas. The
16-
"kernel" is a place holder for the common path segment that all destinations
17-
share:
14+
placeholders can be used to identify common parts of the path.
15+
16+
- The last placeholder is _greedy_ it will match all path segments until the
17+
suffix.
18+
- Preceding placeholders will only match until the first path separator.
19+
20+
Examples
21+
--------
22+
23+
### Navigating between test files
1824

1925
```php
2026
$pathFinder = PathFinder::fromDestinations([
@@ -25,9 +31,25 @@ $pathFinder = PathFinder::fromDestinations([
2531

2632
$targets = $pathFinder->targetsFor('lib/Acme/Post.php');
2733

28-
var_dump($targets);
2934
// [
3035
// 'unit_test' => 'tests/Unit/Acme/PostTest.php',
3136
// 'benchmark' => 'benchmarks/Acme/PostBench.php',
3237
// ]
3338
```
39+
40+
### Navigating between files organized by domain/module
41+
42+
```php
43+
$pathFinder = PathFinder::fromDestinations([
44+
'source' => 'lib/<module>/<kernel>.php',
45+
'unit_test' => 'tests/<module>/Unit/<kernel>Test.php',
46+
'benchmark' => 'benchmarks/<module>/<kernel>Bench.php',
47+
]);
48+
49+
$targets = $pathFinder->targetsFor('lib/MyModule/Acme/Post.php');
50+
51+
// [
52+
// 'unit_test' => 'tests/MyModule/Unit/Acme/PostTest.php',
53+
// 'benchmark' => 'benchmarks/MyModule/Acme/PostBench.php',
54+
// ]
55+
```

composer.json

+12-4
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
"webmozart/path-util": "^2.3"
1313
},
1414
"require-dev": {
15-
"phpunit/phpunit": "~7.0",
16-
"phpstan/phpstan": "~0.11.0",
17-
"friendsofphp/php-cs-fixer": "~2.15.0"
15+
"php": "^7.3",
16+
"phpunit/phpunit": "^9.0",
17+
"phpstan/phpstan": "^0.12.0",
18+
"friendsofphp/php-cs-fixer": "^2.15.0"
1819
},
1920
"autoload": {
2021
"psr-4": {
@@ -26,9 +27,16 @@
2627
"Phpactor\\ClassFileConverter\\Tests\\": "tests/"
2728
}
2829
},
30+
"scripts": {
31+
"integrate": [
32+
"./vendor/bin/phpunit",
33+
"./vendor/bin/phpstan analyze",
34+
"./vendor/bin/php-cs-fixer fix"
35+
]
36+
},
2937
"extra": {
3038
"branch-alias": {
3139
"dev-master": "1.0-dev"
3240
}
3341
}
34-
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Phpactor\ClassFileConverter\Exception;
4+
5+
use RuntimeException;
6+
7+
class NoPlaceHoldersException extends RuntimeException
8+
{
9+
}

lib/PathFinder.php

+32-77
Original file line numberDiff line numberDiff line change
@@ -4,117 +4,72 @@
44

55
use Phpactor\ClassFileConverter\Exception\NoMatchingSourceException;
66
use Webmozart\PathUtil\Path;
7-
use RuntimeException;
87

98
class PathFinder
109
{
11-
const KERNEL = '<kernel>';
12-
1310
/**
14-
* @var array
11+
* @var array<string,Pattern>
1512
*/
1613
private $destinations = [];
1714

15+
/**
16+
* @param array<string, Pattern> $destinations
17+
*/
1818
private function __construct(array $destinations)
1919
{
20-
$this->validateTargets($destinations);
21-
22-
foreach ($destinations as $destinationName => $pattern) {
23-
$this->add($destinationName, $pattern);
24-
}
20+
$this->destinations = $destinations;
2521
}
2622

23+
/**
24+
* @param array<string, string> $destinations
25+
*/
2726
public static function fromDestinations(array $destinations): PathFinder
2827
{
29-
return new self($destinations);
28+
return new self(array_map(function (string $pattern) {
29+
return Pattern::fromPattern($pattern);
30+
}, $destinations));
3031
}
3132

3233
/**
3334
* Return a hash map of destination names to paths representing
3435
* paths which relate to the given file path.
3536
*
3637
* @throws NoMatchingSourceException
38+
* @return array<string,string>
3739
*/
3840
public function destinationsFor(string $filePath): array
3941
{
40-
$filePath = Path::canonicalize($filePath);
41-
$source = $this->matchingSource($filePath);
42-
43-
return $this->resolveDestinations($filePath, $source);
44-
}
42+
$destinations = [];
43+
$sourcePattern = $this->findSourcePattern($filePath);
4544

46-
private function matchingSource($filePath)
47-
{
48-
foreach ($this->destinations as $targetName => $pattern) {
49-
if ($this->matches($filePath, $pattern)) {
50-
return $targetName;
51-
}
52-
}
53-
54-
throw new NoMatchingSourceException(sprintf(
55-
'Could not find a matching pattern for path "%s", known patterns: "%s"',
56-
$filePath,
57-
implode('", "', $this->destinations)
58-
));
59-
}
60-
61-
private function matches(string $filePath, $pattern)
62-
{
63-
$pattern = $this->pattern($pattern);
64-
65-
return (bool) $this->matchPattern($filePath, $pattern);
66-
}
67-
68-
private function resolveDestinations(string $filePath, $target)
69-
{
70-
$resolved = [];
71-
72-
foreach ($this->destinations as $targetName => $targetPattern) {
73-
if ($target === $targetName) {
45+
foreach ($this->destinations as $name => $pattern) {
46+
assert($pattern instanceof Pattern);
47+
if ($pattern === $sourcePattern) {
7448
continue;
7549
}
7650

77-
$sourcePattern = $this->pattern($this->destinations[$target]);
78-
$kernel = $this->matchPattern($filePath, $sourcePattern);
79-
80-
$resolved[$targetName] = str_replace(self::KERNEL, $kernel, $targetPattern);
81-
}
82-
83-
return $resolved;
84-
}
85-
86-
private function pattern(string $pattern): string
87-
{
88-
$pattern = preg_quote($pattern);
89-
90-
return str_replace(preg_quote(self::KERNEL), '(.*?)', $pattern);
91-
}
92-
93-
private function matchPattern(string $filePath, string $pattern)
94-
{
95-
if (preg_match('{' . $pattern . '$}', $filePath, $matches)) {
96-
return $matches[1];
51+
$tokens = $sourcePattern->tokens($filePath);
52+
$destinations[$name] = $pattern->replaceTokens($tokens);
9753
}
9854

99-
return;
55+
return $destinations;
10056
}
10157

102-
private function validateTargets(array $destinations)
58+
private function findSourcePattern(string $filePath): Pattern
10359
{
104-
foreach ($destinations as $destination) {
105-
if (strpos($destination, self::KERNEL)) {
106-
continue;
60+
foreach ($this->destinations as $name => $pattern) {
61+
assert($pattern instanceof Pattern);
62+
if ($pattern->fits($filePath)) {
63+
return $pattern;
10764
}
108-
109-
throw new RuntimeException(sprintf(
110-
'Destination "%s" contains no <kernel> placeholder',
111-
$destination
112-
));
11365
}
114-
}
11566

116-
private function add(string $destinationName, string $pattern)
117-
{
118-
$this->destinations[$destinationName] = $pattern;
67+
throw new NoMatchingSourceException(sprintf(
68+
'Could not find matching source pattern for "%s", known patterns: "%s"',
69+
$filePath,
70+
implode('", "', array_map(function (Pattern $pattern) {
71+
return $pattern->toString();
72+
}, $this->destinations))
73+
));
11974
}
12075
}

lib/Pattern.php

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Phpactor\ClassFileConverter;
4+
5+
use Phpactor\ClassFileConverter\Exception\NoPlaceHoldersException;
6+
use RuntimeException;
7+
use Webmozart\PathUtil\Path;
8+
9+
class Pattern
10+
{
11+
const TOKEN_REGEX = '{<([a-z-]+?)>}';
12+
13+
/**
14+
* @var string
15+
*/
16+
private $regex;
17+
18+
/**
19+
* @var array<string>
20+
*/
21+
private $tokenNames;
22+
23+
/**
24+
* @var string
25+
*/
26+
private $pattern;
27+
28+
/**
29+
* @param array<string> $tokenNames
30+
*/
31+
public function __construct(string $regex, string $pattern, array $tokenNames)
32+
{
33+
$this->regex = $regex;
34+
$this->tokenNames = $tokenNames;
35+
$this->pattern = $pattern;
36+
}
37+
38+
public static function fromPattern(string $pattern): self
39+
{
40+
preg_match_all(self::TOKEN_REGEX, $pattern, $matches);
41+
42+
[$tokens, $tokenNames] = $matches;
43+
44+
$regex = $pattern;
45+
foreach (array_values($matches[0]) as $index => $token) {
46+
$greedy = $index + 1 !== count($tokenNames);
47+
$regex = strtr($regex, [$token => sprintf('(?%s%s+)', $token, $greedy ? '[^/]' : '.')]);
48+
}
49+
50+
if (empty($tokenNames)) {
51+
throw new NoPlaceHoldersException(sprintf(
52+
'File pattern "%s" does not contain any <placeholders>',
53+
$pattern
54+
));
55+
}
56+
57+
return new self(sprintf('{%s$}', $regex), $pattern, $tokenNames);
58+
}
59+
60+
public function fits(string $filePath): bool
61+
{
62+
return (bool)preg_match($this->regex, Path::canonicalize($filePath));
63+
}
64+
65+
/**
66+
* @return array<string, string>
67+
*/
68+
public function tokens(string $filePath): array
69+
{
70+
$filePath = Path::canonicalize($filePath);
71+
72+
if (!preg_match($this->regex, $filePath, $matches)) {
73+
throw new RuntimeException(sprintf(
74+
'Error occurred performing regex on filepath "%s" with regex "%s"',
75+
$filePath,
76+
$this->regex
77+
));
78+
}
79+
80+
return array_intersect_key($matches, (array)array_combine($this->tokenNames, $this->tokenNames));
81+
}
82+
83+
/**
84+
* @param array<string,string> $tokens
85+
*/
86+
public function replaceTokens(array $tokens): string
87+
{
88+
return $this->cleanRemainingTokens($this->replaceTokensWithValues($tokens));
89+
}
90+
91+
public function toString(): string
92+
{
93+
return $this->pattern;
94+
}
95+
96+
/**
97+
* @param array<string,string> $tokens
98+
*/
99+
private function replaceTokensWithValues(array $tokens): string
100+
{
101+
return strtr($this->pattern, (array)array_combine(array_map(function (string $key) {
102+
return '<' . $key . '>';
103+
}, array_keys($tokens)), array_values($tokens)));
104+
}
105+
106+
private function cleanRemainingTokens(string $filePath): string
107+
{
108+
return strtr($filePath, (array)array_combine(array_map(function (string $tokenName) {
109+
return '<' . $tokenName . '>';
110+
}, $this->tokenNames), array_fill(0, count($this->tokenNames), '')));
111+
}
112+
}

phpstan.neon

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
includes:
2-
- vendor/phpstan/phpstan/conf/config.level7.neon
3-
41
parameters:
2+
level: 7
53
inferPrivatePropertyTypeFromConstructor: true
64
ignoreErrors:
75

0 commit comments

Comments
 (0)