diff --git a/analysis-baseline.toml b/analysis-baseline.toml index 6259f5930..f214ee80e 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -438,18 +438,6 @@ code = "unreachable-else-clause" message = "Unreachable else clause" count = 1 -[[issues]] -file = "src/Integration/ModulesIntegration.php" -code = "no-value" -message = "Argument #1 passed to function `array_keys` has type `never`, meaning it cannot produce a value." -count = 1 - -[[issues]] -file = "src/Integration/ModulesIntegration.php" -code = "non-existent-class-like" -message = 'Class, interface, enum, or trait `PackageVersions\Versions` not found.' -count = 1 - [[issues]] file = "src/Integration/RequestIntegration.php" code = "invalid-property-assignment-value" diff --git a/mago.toml b/mago.toml index d1b63f09d..f8b021c00 100644 --- a/mago.toml +++ b/mago.toml @@ -5,6 +5,7 @@ excludes = [ "tests/resources/**", "tests/Fixtures/**", "src/Util/ClockMock.php", + "vendor/open-telemetry/gen-otlp-protobuf/GPBMetadata/**", ] [analyzer] diff --git a/src/Integration/ModulesIntegration.php b/src/Integration/ModulesIntegration.php index 8148b3778..6dd620d24 100644 --- a/src/Integration/ModulesIntegration.php +++ b/src/Integration/ModulesIntegration.php @@ -6,7 +6,6 @@ use Composer\InstalledVersions; use Jean85\PrettyVersions; -use PackageVersions\Versions; use Sentry\Event; use Sentry\SentrySdk; use Sentry\State\Scope; @@ -67,12 +66,19 @@ private static function getInstalledPackages(): array return InstalledVersions::getInstalledPackages(); } - if (class_exists(Versions::class)) { + $versionsClass = 'PackageVersions\\Versions'; + + if (class_exists($versionsClass)) { // BC layer for Composer 1, using a transient dependency - /** @var string[] $packages */ - $packages = array_keys(Versions::VERSIONS); + /** @var mixed $versions */ + $versions = \constant($versionsClass . '::VERSIONS'); + + if (\is_array($versions)) { + /** @var string[] $packages */ + $packages = array_keys($versions); - return $packages; + return $packages; + } } // this should not happen diff --git a/src/Util/CodeLocationResolver.php b/src/Util/CodeLocationResolver.php new file mode 100644 index 000000000..cc968e70c --- /dev/null +++ b/src/Util/CodeLocationResolver.php @@ -0,0 +1,110 @@ +frameBuilder = new FrameBuilder($options, $representationSerializer); + } + + /** + * Resolves the first in-app frame from the current backtrace into code + * location metadata. + * + * @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}|null + */ + public function resolve(int $limit = 20): ?array + { + /** @var list $backtrace */ + $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $limit); + + return $this->resolveFromBacktrace($backtrace); + } + + /** + * Resolves the first in-app frame from a backtrace into code location metadata. + * + * @param array> $backtrace The backtrace + * + * @phpstan-param list $backtrace + * + * @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}|null + */ + public function resolveFromBacktrace(array $backtrace): ?array + { + $frame = $this->findFirstInAppFrameForBacktrace($backtrace); + + if ($frame === null) { + return null; + } + + return $this->getCodeLocationForFrame($frame); + } + + /** + * Find the first in-app frame for a given backtrace. + * + * @param array> $backtrace The backtrace + * + * @phpstan-param list $backtrace + */ + public function findFirstInAppFrameForBacktrace(array $backtrace): ?Frame + { + $file = Frame::INTERNAL_FRAME_FILENAME; + $line = 0; + + foreach ($backtrace as $backtraceFrame) { + $frame = $this->frameBuilder->buildFromBacktraceFrame($file, $line, $backtraceFrame); + + if ($frame->isInApp()) { + return $frame; + } + + $file = $backtraceFrame['file'] ?? Frame::INTERNAL_FRAME_FILENAME; + $line = $backtraceFrame['line'] ?? 0; + } + + return null; + } + + /** + * Converts a frame into code location metadata. + * + * @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int} + */ + public function getCodeLocationForFrame(Frame $frame): array + { + return [ + 'code.filepath' => $frame->getFile(), + 'code.function' => $frame->getFunctionName(), + 'code.lineno' => $frame->getLine(), + ]; + } +} diff --git a/tests/CodeLocationResolverTest.php b/tests/CodeLocationResolverTest.php new file mode 100644 index 000000000..4a45f8a23 --- /dev/null +++ b/tests/CodeLocationResolverTest.php @@ -0,0 +1,94 @@ +createResolver([ + 'prefixes' => [], + ]); + + $frame = $resolver->findFirstInAppFrameForBacktrace($this->createQueryBacktrace($expectedLine)); + + $this->assertNotNull($frame); + $this->assertSame(__FILE__, $frame->getFile()); + $this->assertSame($expectedLine, $frame->getLine()); + $this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $frame->getFunctionName()); + } + + public function testResolveFromBacktraceReturnsCodeLocationMetadata(): void + { + $expectedLine = 321; + $resolver = $this->createResolver([ + 'prefixes' => [\dirname(__DIR__)], + ]); + + $location = $resolver->resolveFromBacktrace($this->createQueryBacktrace($expectedLine)); + + $this->assertNotNull($location); + $this->assertSame(\DIRECTORY_SEPARATOR . 'tests' . \DIRECTORY_SEPARATOR . 'CodeLocationResolverTest.php', $location['code.filepath']); + $this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $location['code.function']); + $this->assertSame($expectedLine, $location['code.lineno']); + } + + public function testResolveFromBacktraceReturnsNullWithoutInAppFrame(): void + { + $resolver = $this->createResolver(); + + $location = $resolver->resolveFromBacktrace([ + [ + 'file' => Frame::INTERNAL_FRAME_FILENAME, + 'line' => 0, + 'function' => 'internal', + ], + [ + 'class' => 'Doctrine\\DBAL\\Connection', + 'function' => 'executeQuery', + ], + ]); + + $this->assertNull($location); + } + + private function createResolver(array $options = []): CodeLocationResolver + { + $options = new Options($options); + + return new CodeLocationResolver($options, new RepresentationSerializer($options)); + } + + /** + * @return array> + */ + private function createQueryBacktrace(int $line): array + { + return [ + [ + 'file' => Frame::INTERNAL_FRAME_FILENAME, + 'line' => 0, + 'function' => 'internal', + ], + [ + 'file' => __FILE__, + 'line' => $line, + 'class' => 'Doctrine\\DBAL\\Connection', + 'function' => 'executeQuery', + ], + [ + 'class' => 'App\\Repository\\UserRepository', + 'function' => 'findActiveUsers', + ], + ]; + } +}