Skip to content

Commit 51ef472

Browse files
authored
cleanup (#1)
* cleanup * misc
1 parent e25168d commit 51ef472

File tree

13 files changed

+310
-107
lines changed

13 files changed

+310
-107
lines changed

.github/workflows/code_analysis.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ on:
99
jobs:
1010
code_analysis:
1111
strategy:
12-
fail-fast: false
1312
matrix:
1413
actions:
1514
-
@@ -36,10 +35,6 @@ jobs:
3635
name: 'Check Active Classes'
3736
run: vendor/bin/class-leak check bin src --ansi
3837

39-
-
40-
name: 'Unusued check'
41-
run: vendor/bin/composer-dependency-analyser
42-
4338
name: ${{ matrix.actions.name }}
4439
runs-on: ubuntu-latest
4540

LICENSE

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
The MIT License
22
---------------
33

4-
Copyright (c) 2020 Tomas Votruba (https://tomasvotruba.com)
4+
Copyright (c) 2025 Tomas Votruba (https://tomasvotruba.com)
55

66
Permission is hereby granted, free of charge, to any person
77
obtaining a copy of this software and associated documentation
@@ -22,4 +22,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
2222
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
2323
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
2424
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25-
OTHER DEALINGS IN THE SOFTWARE.
25+
OTHER DEALINGS IN THE SOFTWARE.

composer-dependency-analyser.php

Lines changed: 0 additions & 7 deletions
This file was deleted.

composer.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"phpstan/phpstan": "^2.0",
2020
"phpunit/phpunit": "^11.5",
2121
"rector/rector": "^2.0",
22-
"shipmonk/composer-dependency-analyser": "^1.8",
2322
"phpecs/phpecs": "^2.0",
2423
"symplify/vendor-patches": "^11.3",
2524
"tomasvotruba/class-leak": "^2.0",
@@ -28,15 +27,15 @@
2827
"autoload": {
2928
"psr-4": {
3029
"Behastan\\": "src"
31-
},
32-
"classmap": [
33-
"stubs"
34-
]
30+
}
3531
},
3632
"autoload-dev": {
3733
"psr-4": {
3834
"Behastan\\Tests\\": "tests"
39-
}
35+
},
36+
"classmap": [
37+
"stubs"
38+
]
4039
},
4140
"replace": {
4241
"symfony/polyfill-ctype": "*",

phpunit.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
<testsuites>
1010
<testsuite name="unit">
1111
<directory>tests</directory>
12-
<exclude>tests/Testing/UnitTestFilePathsFinder/Fixture</exclude>
1312
</testsuite>
1413
</testsuites>
1514
</phpunit>

prefix-code.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ note()
2323
# ---------------------------
2424

2525
# 2. scope it
26-
note "Downloading php-scoper 0.18.11"
27-
wget https://github.com/humbug/php-scoper/releases/download/0.18.11/php-scoper.phar -N --no-verbose
26+
note "Downloading php-scoper 0.18.16"
27+
wget https://github.com/humbug/php-scoper/releases/download/0.18.16/php-scoper.phar -N --no-verbose
2828

2929

3030
note "Running php-scoper"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Behastan\Command;
6+
7+
use Behastan\Finder\BehatMetafilesFinder;
8+
use Behastan\PhpParser\SimplePhpParser;
9+
use Behastan\Resolver\ClassMethodMasksResolver;
10+
use Behastan\ValueObject\ClassMethodContextDefinition;
11+
use PhpParser\Node\Name;
12+
use PhpParser\Node\Stmt\Class_;
13+
use PhpParser\Node\Stmt\ClassMethod;
14+
use PhpParser\NodeFinder;
15+
use PhpParser\PrettyPrinter\Standard;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Webmozart\Assert\Assert;
22+
23+
final class DuplicatedDefinitionsCommand extends Command
24+
{
25+
public function __construct(
26+
private readonly SymfonyStyle $symfonyStyle,
27+
private readonly BehatMetafilesFinder $behatMetafilesFinder,
28+
private readonly SimplePhpParser $simplePhpParser,
29+
private readonly NodeFinder $nodeFinder,
30+
private readonly Standard $printerStandard,
31+
private readonly ClassMethodMasksResolver $classMethodMasksResolver,
32+
) {
33+
parent::__construct();
34+
}
35+
36+
protected function configure(): void
37+
{
38+
$this->setName('duplicated-definitions');
39+
40+
$this->setDescription(
41+
'Find duplicated definitions in *Context.php, use just one to keep definitions clear and to the point'
42+
);
43+
44+
$this->addArgument(
45+
'test-directory',
46+
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
47+
'One or more paths to check or *.Context.php and feature.yml files'
48+
);
49+
}
50+
51+
protected function execute(InputInterface $input, OutputInterface $output): int
52+
{
53+
$testDirectories = (array) $input->getArgument('test-directory');
54+
Assert::allDirectory($testDirectories);
55+
56+
$contextFileInfos = $this->behatMetafilesFinder->findContextFiles($testDirectories);
57+
58+
if ($contextFileInfos === []) {
59+
$this->symfonyStyle->error('No *.Context files found. Please provide correct test directory');
60+
return self::FAILURE;
61+
}
62+
63+
$classMethodContextDefinitionByClassMethodHash = [];
64+
65+
foreach ($contextFileInfos as $contextFileInfo) {
66+
$contextClassStmts = $this->simplePhpParser->parseFilePath($contextFileInfo->getRealPath());
67+
68+
$class = $this->nodeFinder->findFirstInstanceOf($contextClassStmts, Class_::class);
69+
if (! $class instanceof Class_ || ! $class->namespacedName instanceof Name) {
70+
continue;
71+
}
72+
73+
$className = $class->namespacedName->toString();
74+
75+
foreach ($class->getMethods() as $classMethod) {
76+
if (! $classMethod->isPublic() || $classMethod->isMagic()) {
77+
continue;
78+
}
79+
80+
$classMethodHash = $this->createClassMethodHash($classMethod);
81+
82+
$rawMasks = $this->classMethodMasksResolver->resolve($classMethod);
83+
84+
// no masks :(
85+
if (count($rawMasks) === 0) {
86+
continue;
87+
}
88+
89+
$classMethodContextDefinition = new ClassMethodContextDefinition(
90+
$contextFileInfo->getRealPath(),
91+
$className,
92+
$classMethod->name->toString(),
93+
// @todo what about multiple masks?
94+
$rawMasks[0],
95+
$classMethod->getStartLine()
96+
);
97+
98+
$classMethodContextDefinitionByClassMethodHash[$classMethodHash][] = $classMethodContextDefinition;
99+
}
100+
}
101+
102+
// keep only duplicated
103+
foreach ($classMethodContextDefinitionByClassMethodHash as $hash => $classAndMethods) {
104+
if (count($classAndMethods) < 2) {
105+
unset($classMethodContextDefinitionByClassMethodHash[$hash]);
106+
}
107+
108+
}
109+
110+
foreach ($classMethodContextDefinitionByClassMethodHash as $classAndMethods) {
111+
$this->symfonyStyle->warning('Found duplicated class classMethod contents');
112+
113+
foreach ($classAndMethods as $classMethodContextDefinition) {
114+
/** @var ClassMethodContextDefinition $classMethodContextDefinition */
115+
$this->symfonyStyle->writeln(
116+
' * ' . $classMethodContextDefinition->getClass() . '::' . $classMethodContextDefinition->getMethodName()
117+
);
118+
$this->symfonyStyle->writeln(
119+
'in ' . $classMethodContextDefinition->getFilePath() . ':' . $classMethodContextDefinition->getMethodLine()
120+
);
121+
122+
$this->symfonyStyle->writeln('Mask: ' . $classMethodContextDefinition->getMask());
123+
$this->symfonyStyle->newLine();
124+
}
125+
}
126+
127+
// $this->symfonyStyle->error(sprintf('Found %d duplicated class classMethod contents', count($classMethodContextDefinitionByClassMethodHash)));
128+
129+
return Command::FAILURE;
130+
}
131+
132+
private function createClassMethodHash(ClassMethod $classMethod): string
133+
{
134+
$printedClassMethod = $this->printerStandard->prettyPrint((array) $classMethod->stmts);
135+
return sha1($printedClassMethod);
136+
}
137+
}

src/DefinitionMasksResolver.php

Lines changed: 21 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,27 @@
44

55
namespace Behastan;
66

7+
use Behastan\PhpParser\SimplePhpParser;
8+
use Behastan\Resolver\ClassMethodMasksResolver;
79
use Behastan\ValueObject\ClassMethodContextDefinition;
810
use Behastan\ValueObject\Mask\ExactMask;
911
use Behastan\ValueObject\Mask\NamedMask;
1012
use Behastan\ValueObject\Mask\RegexMask;
1113
use Behastan\ValueObject\Mask\SkippedMask;
1214
use Behastan\ValueObject\MaskCollection;
13-
use PhpParser\Comment\Doc;
1415
use PhpParser\Node\Name;
15-
use PhpParser\Node\Scalar\String_;
16-
use PhpParser\Node\Stmt;
1716
use PhpParser\Node\Stmt\Class_;
1817
use PhpParser\NodeFinder;
19-
use PhpParser\NodeTraverser;
20-
use PhpParser\NodeVisitor\NameResolver;
21-
use PhpParser\ParserFactory;
2218
use SplFileInfo;
2319

2420
final class DefinitionMasksResolver
2521
{
26-
/**
27-
* @var string
28-
*/
29-
private const INSTRUCTION_DOCBLOCK_REGEX = '#\@(Given|Then|When)\s+(?<instruction>.*?)\n#m';
30-
31-
/**
32-
* @var string[]
33-
*/
34-
private const ATTRIBUTE_NAMES = ['Behat\Step\Then', 'Behat\Step\Given', 'Behat\Step\And'];
22+
public function __construct(
23+
private readonly SimplePhpParser $simplePhpParser,
24+
private readonly NodeFinder $nodeFinder,
25+
private readonly ClassMethodMasksResolver $classMethodMasksResolver,
26+
) {
27+
}
3528

3629
/**
3730
* @param SplFileInfo[] $contextFiles
@@ -94,97 +87,42 @@ public function resolve(array $contextFiles): MaskCollection
9487

9588
/**
9689
* @param SplFileInfo[] $fileInfos
97-
*
9890
* @return ClassMethodContextDefinition[]
9991
*/
10092
private function resolveMasksFromFiles(array $fileInfos): array
10193
{
10294
$classMethodContextDefinitions = [];
10395

104-
$parserFactory = new ParserFactory();
105-
$nodeFinder = new NodeFinder();
106-
107-
$phpParser = $parserFactory->createForHostVersion();
108-
$nodeTraverser = new NodeTraverser();
109-
$nodeTraverser->addVisitor(new NameResolver());
110-
11196
foreach ($fileInfos as $fileInfo) {
112-
/** @var string $fileContents */
113-
$fileContents = file_get_contents($fileInfo->getRealPath());
114-
115-
/** @var Stmt[] $stmts */
116-
$stmts = $phpParser->parse($fileContents);
117-
$nodeTraverser->traverse($stmts);
97+
$stmts = $this->simplePhpParser->parseFilePath($fileInfo->getRealPath());
11898

11999
// 1. get class name
120-
$class = $nodeFinder->findFirstInstanceOf($stmts, Class_::class);
100+
$class = $this->nodeFinder->findFirstInstanceOf($stmts, Class_::class);
121101
if (! $class instanceof Class_) {
122102
continue;
123103
}
124104

125-
if ($class->isAnonymous()) {
126-
continue;
127-
}
128-
129-
if (! $class->namespacedName instanceof Name) {
105+
// is magic class?
106+
if ($class->isAnonymous() || ! $class->namespacedName instanceof Name) {
130107
continue;
131108
}
132109

133110
$className = $class->namespacedName->toString();
134111

135112
foreach ($class->getMethods() as $classMethod) {
136-
$methodName = $classMethod->name->toString();
137-
138-
// 1. collect from docblock
139-
if ($classMethod->getDocComment() instanceof Doc) {
140-
preg_match_all(self::INSTRUCTION_DOCBLOCK_REGEX, $classMethod->getDocComment()->getText(), $match);
141-
142-
foreach ($match['instruction'] as $instruction) {
143-
$mask = $this->clearMask($instruction);
144-
145-
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
146-
$fileInfo->getRealPath(),
147-
$className,
148-
$methodName,
149-
$mask
150-
);
151-
}
152-
}
153-
154-
// 2. collect from attributes
155-
foreach ($classMethod->attrGroups as $attrGroup) {
156-
foreach ($attrGroup->attrs as $attr) {
157-
$attributeName = $attr->name->toString();
158-
if (! in_array($attributeName, self::ATTRIBUTE_NAMES)) {
159-
continue;
160-
}
161-
162-
$firstArgValue = $attr->args[0]->value;
163-
164-
if (! $firstArgValue instanceof String_) {
165-
continue;
166-
}
167-
168-
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
169-
$fileInfo->getRealPath(),
170-
$className,
171-
$methodName,
172-
$firstArgValue->value
173-
);
174-
}
113+
$rawMasks = $this->classMethodMasksResolver->resolve($classMethod);
114+
115+
foreach ($rawMasks as $rawMask) {
116+
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
117+
$fileInfo->getRealPath(),
118+
$className,
119+
$classMethod->name->toString(),
120+
$rawMask
121+
);
175122
}
176123
}
177124
}
178125

179126
return $classMethodContextDefinitions;
180127
}
181-
182-
private function clearMask(string $mask): string
183-
{
184-
$mask = trim($mask);
185-
186-
// clear extra quote escaping that would cause miss-match with feature masks
187-
$mask = str_replace('\\\'', "'", $mask);
188-
return str_replace('\\/', '/', $mask);
189-
}
190128
}

0 commit comments

Comments
 (0)