From 5d3766b75fc3d8c066b8a0a8c8fa2a10acd02e5d Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 17 Mar 2026 09:49:39 +0100 Subject: [PATCH 1/2] fix(phpstan): resolve preg_match offset access errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use non-capturing groups for unused captures in regex patterns and assert match offsets exist, so PHPStan can verify safe array access. 🤖 Generated with Claude Code --- src/Chromosome.php | 5 +++-- src/GenomicPosition.php | 6 ++++-- src/GenomicRegion.php | 8 +++++--- src/IlluminaRunFolder.php | 4 ++++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Chromosome.php b/src/Chromosome.php index 432add0..390c4ab 100644 --- a/src/Chromosome.php +++ b/src/Chromosome.php @@ -14,11 +14,12 @@ class Chromosome public function __construct(string $value) { /** Matches human chromosomes with or without "chr" prefix: chr1-chr22, chrX, chrY, chrM, chrMT, or 1-22, X, Y, M, MT. */ - if (preg_match('/^(chr)?(1[0-9]|[1-9]|2[0-2]|X|Y|M|MT)$/i', $value, $matches) === 0) { + if (preg_match('/^(?:chr)?(1[0-9]|[1-9]|2[0-2]|X|Y|M|MT)$/i', $value, $matches) === 0) { throw new \InvalidArgumentException("Invalid chromosome: {$value}. Expected format: chr1-chr22, chrX, chrY, chrM, or without chr prefix."); } - $value = strtoupper($matches[2]); + assert(isset($matches[1])); + $value = strtoupper($matches[1]); $this->value = $value === self::MITOCHONDRIAL_ENSEMBL ? self::MITOCHONDRIAL : $value; } diff --git a/src/GenomicPosition.php b/src/GenomicPosition.php index bdf4eb3..0586874 100644 --- a/src/GenomicPosition.php +++ b/src/GenomicPosition.php @@ -23,11 +23,13 @@ public function __construct(Chromosome $chromosome, int $position) /** @example GenomicPosition::parse('chr1:123456') */ public static function parse(string $value): self { - if (preg_match('/^([^:]+):(g\.|)(\d+)$/', $value, $matches) === 0) { + if (preg_match('/^([^:]+):(?:g\.)?(\d+)$/', $value, $matches) === 0) { throw new \InvalidArgumentException("Invalid genomic position format: {$value}. Expected format: chr1:123456."); } - return new self(new Chromosome($matches[1]), (int) $matches[3]); + assert(isset($matches[1], $matches[2])); + + return new self(new Chromosome($matches[1]), (int) $matches[2]); } public function equals(self $other): bool diff --git a/src/GenomicRegion.php b/src/GenomicRegion.php index 3acf3a1..e7bd619 100644 --- a/src/GenomicRegion.php +++ b/src/GenomicRegion.php @@ -36,14 +36,16 @@ public function __construct( public static function parse(string $value): self { - if (preg_match('/^([^:]+):(g\.|)(\d+)(-(\d+)|)$/', $value, $matches) === 0) { + if (preg_match('/^([^:]+):(?:g\.)?(\d+)(?:-(\d+))?$/', $value, $matches) === 0) { throw new \InvalidArgumentException("Invalid genomic region format: {$value}. Expected format: chr1:123-456."); } + assert(isset($matches[1], $matches[2])); + return new self( new Chromosome($matches[1]), - (int) $matches[3], - (int) ($matches[5] ?? $matches[3]) + (int) $matches[2], + (int) ($matches[3] ?? $matches[2]) ); } diff --git a/src/IlluminaRunFolder.php b/src/IlluminaRunFolder.php index b371c9f..9428e34 100644 --- a/src/IlluminaRunFolder.php +++ b/src/IlluminaRunFolder.php @@ -94,10 +94,14 @@ public static function parse(string $runFolder): self private static function extractFlowcellID(string $flowcellSegment): string { if (preg_match(self::ZERO_PREFIXED_PATTERN, $flowcellSegment, $matches) === 1) { + assert(isset($matches[1])); + return $matches[1]; } if (preg_match(self::SIDE_PREFIXED_PATTERN, $flowcellSegment, $matches) === 1) { + assert(isset($matches[1])); + return $matches[1]; } From dbbc776b87d3d7e0545f7f32c6a2774ed40c01a8 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 17 Mar 2026 10:05:27 +0100 Subject: [PATCH 2/2] refactor: use SafeCast::toInt for regex match conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw (int) casts on preg_match captures with SafeCast::toInt to ensure invalid values throw rather than silently converting to 0. 🤖 Generated with Claude Code --- src/Chromosome.php | 4 +++- src/GenomicPosition.php | 5 ++++- src/GenomicRegion.php | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Chromosome.php b/src/Chromosome.php index 390c4ab..33dc549 100644 --- a/src/Chromosome.php +++ b/src/Chromosome.php @@ -20,7 +20,9 @@ public function __construct(string $value) assert(isset($matches[1])); $value = strtoupper($matches[1]); - $this->value = $value === self::MITOCHONDRIAL_ENSEMBL ? self::MITOCHONDRIAL : $value; + $this->value = $value === self::MITOCHONDRIAL_ENSEMBL + ? self::MITOCHONDRIAL + : $value; } public function value(): string diff --git a/src/GenomicPosition.php b/src/GenomicPosition.php index 0586874..f66e061 100644 --- a/src/GenomicPosition.php +++ b/src/GenomicPosition.php @@ -29,7 +29,10 @@ public static function parse(string $value): self assert(isset($matches[1], $matches[2])); - return new self(new Chromosome($matches[1]), (int) $matches[2]); + return new self( + new Chromosome($matches[1]), + SafeCast::toInt($matches[2]) + ); } public function equals(self $other): bool diff --git a/src/GenomicRegion.php b/src/GenomicRegion.php index e7bd619..793897e 100644 --- a/src/GenomicRegion.php +++ b/src/GenomicRegion.php @@ -44,8 +44,8 @@ public static function parse(string $value): self return new self( new Chromosome($matches[1]), - (int) $matches[2], - (int) ($matches[3] ?? $matches[2]) + SafeCast::toInt($matches[2]), + SafeCast::toInt($matches[3] ?? $matches[2]) ); }