From 02e6b9e246f9a7725f97792e9816aecafce19ca0 Mon Sep 17 00:00:00 2001 From: levnikolaevich Date: Sat, 4 Apr 2026 16:39:20 +0200 Subject: [PATCH 1/2] Fix Windows composer path and runtime isolation --- src/Composer/Composer.php | 17 +++++++++++++++-- src/Indexer.php | 16 +++++++++++++++- tests/Composer/ComposerTest.php | 11 +++++++++++ tests/Indexer/IndexerTest.php | 12 ++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 5aefd666..b8a8195f 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -82,6 +82,16 @@ private static function join(string $elem, string ...$elems): string return implode(DIRECTORY_SEPARATOR, [$elem, ...$elems]); } + /** @return non-empty-string */ + private static function trimPathSeparators(string $path): string + { + $trimmed = rtrim($path, '/\\'); + if ($trimmed === '') { + return $path; + } + return $trimmed; + } + /** @param non-empty-string $projectRoot */ public function __construct(private readonly string $projectRoot) { @@ -123,7 +133,7 @@ public function __construct(private readonly string $projectRoot) is_array($json['config'] ?? null) && is_string($json['config']['vendor-dir'] ?? null) ) { - $dir = trim($json['config']['vendor-dir'], '/'); + $dir = trim($json['config']['vendor-dir'], '/\\'); if ($dir !== '') { $vendorDir = $dir; } @@ -137,6 +147,9 @@ public function __construct(private readonly string $projectRoot) if (!$loader instanceof ClassLoader) { throw new RuntimeException("Cannot get autoload.php class loader."); } + if ($autoloadDir !== $this->scipPhpVendorDir) { + $loader->unregister(); + } $this->loader = $loader; $installed = require self::join($this->vendorDir, 'composer', 'installed.php'); @@ -279,7 +292,7 @@ private function loadProjectFiles(array $autoload): array continue; } $p = self::join($this->projectRoot, $path); - $p = rtrim($p, DIRECTORY_SEPARATOR); + $p = self::trimPathSeparators($p); $generator->scanPaths($p, $exclusionRegex, $t, $ns); } } diff --git a/src/Indexer.php b/src/Indexer.php index 40b9bd34..d9612b9c 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -17,6 +17,8 @@ use function array_values; use function str_replace; +use function str_starts_with; +use function substr; final readonly class Indexer { @@ -57,6 +59,18 @@ public function __construct( $this->types = new Types($this->composer, $this->namer); } + /** @return non-empty-string */ + private function relativePath(string $filename): string + { + $normalizedProjectRoot = str_replace('\\', '/', $this->projectRoot); + $normalizedFilename = str_replace('\\', '/', $filename); + $prefix = $normalizedProjectRoot . '/'; + if (str_starts_with($normalizedFilename, $prefix)) { + return substr($normalizedFilename, \strlen($prefix)); + } + return $normalizedFilename; + } + public function index(): Index { $projectFiles = $this->composer->projectFiles(); @@ -69,7 +83,7 @@ public function index(): Index $this->parser->traverse($filename, $indexer, $indexer->index(...)); $documents[] = new Document([ 'language' => Language::PHP, - 'relative_path' => str_replace($this->projectRoot . '/', '', $filename), + 'relative_path' => $this->relativePath($filename), 'occurrences' => $indexer->occurrences, 'symbols' => array_values($indexer->symbols), 'position_encoding' => PositionEncoding::UTF8CodeUnitOffsetFromLineStart, diff --git a/tests/Composer/ComposerTest.php b/tests/Composer/ComposerTest.php index 7568466a..562fcef6 100644 --- a/tests/Composer/ComposerTest.php +++ b/tests/Composer/ComposerTest.php @@ -6,6 +6,7 @@ use Override; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use ScipPhp\Composer\Composer; use function count; @@ -82,6 +83,16 @@ public function testProjectFiles(): void self::assertStringEndsWith(self::join($root, 'tests', 'ClassATestCase.php'), $files[4]); } + public function testTrimPathSeparatorsSupportsBothSlashStyles(): void + { + $method = new ReflectionMethod(Composer::class, 'trimPathSeparators'); + + self::assertSame('foo/bar', $method->invoke(null, 'foo/bar/')); + self::assertSame('foo\\bar', $method->invoke(null, 'foo\\bar\\')); + self::assertSame('foo/bar', $method->invoke(null, 'foo/bar\\')); + self::assertSame('foo\\bar', $method->invoke(null, 'foo\\bar/')); + } + public function testIsDependency(): void { foreach ([...self::BUILTIN, ...self::DEPS, ...self::UNKNOWN] as $idents) { diff --git a/tests/Indexer/IndexerTest.php b/tests/Indexer/IndexerTest.php index 518221fd..18a5fd31 100644 --- a/tests/Indexer/IndexerTest.php +++ b/tests/Indexer/IndexerTest.php @@ -7,6 +7,7 @@ use Override; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ScipPhp\File\Reader; @@ -91,6 +92,17 @@ public function testIndexer(): void } } + public function testRelativePathNormalizesWindowsSeparators(): void + { + $indexer = new Indexer(self::TESTDATA_DIR . 'scip-php-test', 'test', []); + $method = new ReflectionMethod(Indexer::class, 'relativePath'); + + self::assertSame( + 'src/Foo.php', + $method->invoke($indexer, self::TESTDATA_DIR . 'scip-php-test\\src\\Foo.php'), + ); + } + /** @return array */ private static function files(string $dir): array { From d587585229adabe924e805e2aeda29c9da8b5b69 Mon Sep 17 00:00:00 2001 From: levnikolaevich Date: Sat, 4 Apr 2026 16:53:28 +0200 Subject: [PATCH 2/2] Detect installed vendor directory correctly --- src/Composer/Composer.php | 25 +++++++++++++++++++++++-- tests/Composer/ComposerTest.php | 10 ++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index b8a8195f..ee036749 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -92,6 +92,27 @@ private static function trimPathSeparators(string $path): string return $trimmed; } + /** @return ?non-empty-string */ + private static function discoverScipPhpVendorDir(): ?string + { + $packageRoot = self::join(__DIR__, '..', '..'); + $candidates = [ + self::join($packageRoot, 'vendor'), + self::join($packageRoot, '..', '..'), + ]; + foreach ($candidates as $candidate) { + $autoload = self::join($candidate, 'autoload.php'); + if (!is_file($autoload)) { + continue; + } + $realPath = realpath($candidate); + if ($realPath !== false) { + return $realPath; + } + } + return null; + } + /** @param non-empty-string $projectRoot */ public function __construct(private readonly string $projectRoot) { @@ -99,8 +120,8 @@ public function __construct(private readonly string $projectRoot) $autoload = is_array($json['autoload'] ?? null) ? $json['autoload'] : []; $autoloadDev = is_array($json['autoload-dev'] ?? null) ? $json['autoload-dev'] : []; - $scipPhpVendorDir = self::join(__DIR__, '..', '..', 'vendor'); - if (realpath($scipPhpVendorDir) === false) { + $scipPhpVendorDir = self::discoverScipPhpVendorDir(); + if ($scipPhpVendorDir === null) { // If the vendor directory relative to this file is not found, scip-php probably runs as a // dev dependency of the project that it analyses and shares the vendor directory with it. $cwd = getcwd(); diff --git a/tests/Composer/ComposerTest.php b/tests/Composer/ComposerTest.php index 562fcef6..67b74bf2 100644 --- a/tests/Composer/ComposerTest.php +++ b/tests/Composer/ComposerTest.php @@ -93,6 +93,16 @@ public function testTrimPathSeparatorsSupportsBothSlashStyles(): void self::assertSame('foo\\bar', $method->invoke(null, 'foo\\bar/')); } + public function testDiscoverScipPhpVendorDirFindsOwnVendorDirectory(): void + { + $method = new ReflectionMethod(Composer::class, 'discoverScipPhpVendorDir'); + $vendorDir = $method->invoke(null); + + self::assertIsString($vendorDir); + self::assertStringEndsWith(self::join('vendor'), $vendorDir); + self::assertFileExists(self::join($vendorDir, 'autoload.php')); + } + public function testIsDependency(): void { foreach ([...self::BUILTIN, ...self::DEPS, ...self::UNKNOWN] as $idents) {