S004 Edge Cases
| ID | Scenario | Expected Behavior |
|---|---|---|
| S004-EC-001 | File content contains class as a string literal inside a Pest test (e.g. $x = 'class Foo extends Bar {';) but no actual class declaration | PestCoversLinker::supports() MUST return true only if the regex \bclass\s+\w+\s*(extends|implements|\{) does not match. A string literal containing class Foo extends will cause a false negative (linker rejects the file). This is a known limitation of regex-based detection. |
| S004-EC-002 | covers() argument uses a leading backslash: \App\Services\FooService::class | resolveClassReference() MUST strip the leading backslash and return App\Services\FooService. |
| S004-EC-003 | Use statement with alias: use App\Services\FooService as Foo; and coverage declaration ->covers(Foo::class) | resolveClassReference() MUST look up Foo in the $useMap (which maps Foo => App\Services\FooService) and return App\Services\FooService. |
| S004-EC-004 | Reference contains a sub-namespace: Services\FooService::class with $useMap['Services'] = 'App\Services' | resolveClassReference() MUST resolve the first segment Services via $useMap to App\Services, then append FooService, returning App\Services\FooService. |
| S004-EC-005 | Bare class name without use map or namespace: FooService::class with $useMap = [] and $namespace = null | resolveClassReference() MUST return the bare name FooService (no resolution possible, returned as-is). |
| S004-EC-006 | Empty or whitespace-only test file content | supports() MUST return false for both linkers. extractCoveredClasses() MUST return [] if called. |
| S004-EC-007 | covers() argument is a string literal instead of ::class: ->covers('App\Services\FooService') | resolveClassReference() MUST parse the string literal and return App\Services\FooService (unescape double backslashes). |
| S004-EC-008 | #[CoversClass()] attribute appears inside the class body (on a method) | PhpAttributeLinker MUST ignore it because extraction only scans the portion before the first class keyword (per S004-FR-012). |
| S004-EC-009 | fromConfig() receives an array with an unknown linker name: ['pest-covers', 'unknown-linker'] | The unknown name MUST be silently skipped. The registry MUST contain only PestCoversLinker. No exception thrown. |
| S004-EC-010 | File has both a class declaration and Pest-style it() calls (hybrid file) | PestCoversLinker::supports() MUST return false (class declaration takes precedence). PhpAttributeLinker::supports() MUST return true. The registry will select PhpAttributeLinker. |
| S004-EC-011 | Same class referenced multiple times: ->covers(FooService::class, FooService::class) | PestCoversLinker::extractCoveredClasses() MUST deduplicate and return exactly one entry for FooService. |
| S004-EC-012 | Test file has non-CoversClass attributes: #[Group('unit')], #[DataProvider('data')] | PhpAttributeLinker MUST ignore all attributes whose name does not match the configured attribute short name. Extracted classes MUST be empty if no CoversClass attribute is present. |
| S004-EC-013 | resolveClassReference() receives an argument that is not a ::class reference, not a string literal (e.g. 42, $variable, SomeClass::METHOD) | MUST return null. No exception thrown. |
| S004-EC-014 | Unbalanced brackets in attribute: #[CoversClass(Foo::class (missing closing bracket or paren) | PhpAttributeLinker MUST handle gracefully without throwing an exception. The malformed attribute MUST be skipped. The extractBalancedParens() method returns null for unbalanced input. |

