CodeUnit.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of sebastian/code-unit.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\CodeUnit;
  11. use function count;
  12. use function file;
  13. use function file_exists;
  14. use function is_readable;
  15. use function range;
  16. use function sprintf;
  17. use ReflectionClass;
  18. use ReflectionFunction;
  19. use ReflectionMethod;
  20. /**
  21. * @psalm-immutable
  22. */
  23. abstract readonly class CodeUnit
  24. {
  25. /**
  26. * @psalm-var non-empty-string
  27. */
  28. private string $name;
  29. /**
  30. * @psalm-var non-empty-string
  31. */
  32. private string $sourceFileName;
  33. /**
  34. * @psalm-var list<int>
  35. */
  36. private array $sourceLines;
  37. /**
  38. * @psalm-param class-string $className
  39. *
  40. * @throws InvalidCodeUnitException
  41. * @throws ReflectionException
  42. */
  43. public static function forClass(string $className): ClassUnit
  44. {
  45. self::ensureUserDefinedClass($className);
  46. $reflector = self::reflectorForClass($className);
  47. return new ClassUnit(
  48. $className,
  49. $reflector->getFileName(),
  50. range(
  51. $reflector->getStartLine(),
  52. $reflector->getEndLine(),
  53. ),
  54. );
  55. }
  56. /**
  57. * @psalm-param class-string $className
  58. *
  59. * @throws InvalidCodeUnitException
  60. * @throws ReflectionException
  61. */
  62. public static function forClassMethod(string $className, string $methodName): ClassMethodUnit
  63. {
  64. self::ensureUserDefinedClass($className);
  65. $reflector = self::reflectorForClassMethod($className, $methodName);
  66. return new ClassMethodUnit(
  67. $className . '::' . $methodName,
  68. $reflector->getFileName(),
  69. range(
  70. $reflector->getStartLine(),
  71. $reflector->getEndLine(),
  72. ),
  73. );
  74. }
  75. /**
  76. * @psalm-param non-empty-string $path
  77. *
  78. * @throws InvalidCodeUnitException
  79. */
  80. public static function forFileWithAbsolutePath(string $path): FileUnit
  81. {
  82. self::ensureFileExistsAndIsReadable($path);
  83. return new FileUnit(
  84. $path,
  85. $path,
  86. range(
  87. 1,
  88. count(file($path)),
  89. ),
  90. );
  91. }
  92. /**
  93. * @psalm-param class-string $interfaceName
  94. *
  95. * @throws InvalidCodeUnitException
  96. * @throws ReflectionException
  97. */
  98. public static function forInterface(string $interfaceName): InterfaceUnit
  99. {
  100. self::ensureUserDefinedInterface($interfaceName);
  101. $reflector = self::reflectorForClass($interfaceName);
  102. return new InterfaceUnit(
  103. $interfaceName,
  104. $reflector->getFileName(),
  105. range(
  106. $reflector->getStartLine(),
  107. $reflector->getEndLine(),
  108. ),
  109. );
  110. }
  111. /**
  112. * @psalm-param class-string $interfaceName
  113. *
  114. * @throws InvalidCodeUnitException
  115. * @throws ReflectionException
  116. */
  117. public static function forInterfaceMethod(string $interfaceName, string $methodName): InterfaceMethodUnit
  118. {
  119. self::ensureUserDefinedInterface($interfaceName);
  120. $reflector = self::reflectorForClassMethod($interfaceName, $methodName);
  121. return new InterfaceMethodUnit(
  122. $interfaceName . '::' . $methodName,
  123. $reflector->getFileName(),
  124. range(
  125. $reflector->getStartLine(),
  126. $reflector->getEndLine(),
  127. ),
  128. );
  129. }
  130. /**
  131. * @psalm-param class-string $traitName
  132. *
  133. * @throws InvalidCodeUnitException
  134. * @throws ReflectionException
  135. */
  136. public static function forTrait(string $traitName): TraitUnit
  137. {
  138. self::ensureUserDefinedTrait($traitName);
  139. $reflector = self::reflectorForClass($traitName);
  140. return new TraitUnit(
  141. $traitName,
  142. $reflector->getFileName(),
  143. range(
  144. $reflector->getStartLine(),
  145. $reflector->getEndLine(),
  146. ),
  147. );
  148. }
  149. /**
  150. * @psalm-param class-string $traitName
  151. *
  152. * @throws InvalidCodeUnitException
  153. * @throws ReflectionException
  154. */
  155. public static function forTraitMethod(string $traitName, string $methodName): TraitMethodUnit
  156. {
  157. self::ensureUserDefinedTrait($traitName);
  158. $reflector = self::reflectorForClassMethod($traitName, $methodName);
  159. return new TraitMethodUnit(
  160. $traitName . '::' . $methodName,
  161. $reflector->getFileName(),
  162. range(
  163. $reflector->getStartLine(),
  164. $reflector->getEndLine(),
  165. ),
  166. );
  167. }
  168. /**
  169. * @psalm-param callable-string $functionName
  170. *
  171. * @throws InvalidCodeUnitException
  172. * @throws ReflectionException
  173. */
  174. public static function forFunction(string $functionName): FunctionUnit
  175. {
  176. $reflector = self::reflectorForFunction($functionName);
  177. if (!$reflector->isUserDefined()) {
  178. throw new InvalidCodeUnitException(
  179. sprintf(
  180. '"%s" is not a user-defined function',
  181. $functionName,
  182. ),
  183. );
  184. }
  185. return new FunctionUnit(
  186. $functionName,
  187. $reflector->getFileName(),
  188. range(
  189. $reflector->getStartLine(),
  190. $reflector->getEndLine(),
  191. ),
  192. );
  193. }
  194. /**
  195. * @psalm-param non-empty-string $name
  196. * @psalm-param non-empty-string $sourceFileName
  197. * @psalm-param list<int> $sourceLines
  198. */
  199. private function __construct(string $name, string $sourceFileName, array $sourceLines)
  200. {
  201. $this->name = $name;
  202. $this->sourceFileName = $sourceFileName;
  203. $this->sourceLines = $sourceLines;
  204. }
  205. /**
  206. * @psalm-return non-empty-string
  207. */
  208. public function name(): string
  209. {
  210. return $this->name;
  211. }
  212. /**
  213. * @psalm-return non-empty-string
  214. */
  215. public function sourceFileName(): string
  216. {
  217. return $this->sourceFileName;
  218. }
  219. /**
  220. * @psalm-return list<int>
  221. */
  222. public function sourceLines(): array
  223. {
  224. return $this->sourceLines;
  225. }
  226. /**
  227. * @psalm-assert-if-true ClassUnit $this
  228. */
  229. public function isClass(): bool
  230. {
  231. return false;
  232. }
  233. /**
  234. * @psalm-assert-if-true ClassMethodUnit $this
  235. */
  236. public function isClassMethod(): bool
  237. {
  238. return false;
  239. }
  240. /**
  241. * @psalm-assert-if-true InterfaceUnit $this
  242. */
  243. public function isInterface(): bool
  244. {
  245. return false;
  246. }
  247. /**
  248. * @psalm-assert-if-true InterfaceMethodUnit $this
  249. */
  250. public function isInterfaceMethod(): bool
  251. {
  252. return false;
  253. }
  254. /**
  255. * @psalm-assert-if-true TraitUnit $this
  256. */
  257. public function isTrait(): bool
  258. {
  259. return false;
  260. }
  261. /**
  262. * @psalm-assert-if-true TraitMethodUnit $this
  263. */
  264. public function isTraitMethod(): bool
  265. {
  266. return false;
  267. }
  268. /**
  269. * @psalm-assert-if-true FunctionUnit $this
  270. */
  271. public function isFunction(): bool
  272. {
  273. return false;
  274. }
  275. /**
  276. * @psalm-assert-if-true FileUnit $this
  277. */
  278. public function isFile(): bool
  279. {
  280. return false;
  281. }
  282. /**
  283. * @psalm-param non-empty-string $path
  284. *
  285. * @throws InvalidCodeUnitException
  286. */
  287. private static function ensureFileExistsAndIsReadable(string $path): void
  288. {
  289. if (!(file_exists($path) && is_readable($path))) {
  290. throw new InvalidCodeUnitException(
  291. sprintf(
  292. 'File "%s" does not exist or is not readable',
  293. $path,
  294. ),
  295. );
  296. }
  297. }
  298. /**
  299. * @psalm-param class-string $className
  300. *
  301. * @throws InvalidCodeUnitException
  302. */
  303. private static function ensureUserDefinedClass(string $className): void
  304. {
  305. try {
  306. $reflector = new ReflectionClass($className);
  307. if ($reflector->isInterface()) {
  308. throw new InvalidCodeUnitException(
  309. sprintf(
  310. '"%s" is an interface and not a class',
  311. $className,
  312. ),
  313. );
  314. }
  315. if ($reflector->isTrait()) {
  316. throw new InvalidCodeUnitException(
  317. sprintf(
  318. '"%s" is a trait and not a class',
  319. $className,
  320. ),
  321. );
  322. }
  323. if (!$reflector->isUserDefined()) {
  324. throw new InvalidCodeUnitException(
  325. sprintf(
  326. '"%s" is not a user-defined class',
  327. $className,
  328. ),
  329. );
  330. }
  331. // @codeCoverageIgnoreStart
  332. } catch (\ReflectionException $e) {
  333. throw new ReflectionException(
  334. $e->getMessage(),
  335. $e->getCode(),
  336. $e,
  337. );
  338. }
  339. // @codeCoverageIgnoreEnd
  340. }
  341. /**
  342. * @psalm-param class-string $interfaceName
  343. *
  344. * @throws InvalidCodeUnitException
  345. */
  346. private static function ensureUserDefinedInterface(string $interfaceName): void
  347. {
  348. try {
  349. $reflector = new ReflectionClass($interfaceName);
  350. if (!$reflector->isInterface()) {
  351. throw new InvalidCodeUnitException(
  352. sprintf(
  353. '"%s" is not an interface',
  354. $interfaceName,
  355. ),
  356. );
  357. }
  358. if (!$reflector->isUserDefined()) {
  359. throw new InvalidCodeUnitException(
  360. sprintf(
  361. '"%s" is not a user-defined interface',
  362. $interfaceName,
  363. ),
  364. );
  365. }
  366. // @codeCoverageIgnoreStart
  367. } catch (\ReflectionException $e) {
  368. throw new ReflectionException(
  369. $e->getMessage(),
  370. $e->getCode(),
  371. $e,
  372. );
  373. }
  374. // @codeCoverageIgnoreEnd
  375. }
  376. /**
  377. * @psalm-param class-string $traitName
  378. *
  379. * @throws InvalidCodeUnitException
  380. */
  381. private static function ensureUserDefinedTrait(string $traitName): void
  382. {
  383. try {
  384. $reflector = new ReflectionClass($traitName);
  385. if (!$reflector->isTrait()) {
  386. throw new InvalidCodeUnitException(
  387. sprintf(
  388. '"%s" is not a trait',
  389. $traitName,
  390. ),
  391. );
  392. }
  393. // @codeCoverageIgnoreStart
  394. if (!$reflector->isUserDefined()) {
  395. throw new InvalidCodeUnitException(
  396. sprintf(
  397. '"%s" is not a user-defined trait',
  398. $traitName,
  399. ),
  400. );
  401. }
  402. } catch (\ReflectionException $e) {
  403. throw new ReflectionException(
  404. $e->getMessage(),
  405. $e->getCode(),
  406. $e,
  407. );
  408. }
  409. // @codeCoverageIgnoreEnd
  410. }
  411. /**
  412. * @psalm-param class-string $className
  413. *
  414. * @throws ReflectionException
  415. */
  416. private static function reflectorForClass(string $className): ReflectionClass
  417. {
  418. try {
  419. return new ReflectionClass($className);
  420. // @codeCoverageIgnoreStart
  421. } catch (\ReflectionException $e) {
  422. throw new ReflectionException(
  423. $e->getMessage(),
  424. $e->getCode(),
  425. $e,
  426. );
  427. }
  428. // @codeCoverageIgnoreEnd
  429. }
  430. /**
  431. * @psalm-param class-string $className
  432. *
  433. * @throws ReflectionException
  434. */
  435. private static function reflectorForClassMethod(string $className, string $methodName): ReflectionMethod
  436. {
  437. try {
  438. return new ReflectionMethod($className, $methodName);
  439. // @codeCoverageIgnoreStart
  440. } catch (\ReflectionException $e) {
  441. throw new ReflectionException(
  442. $e->getMessage(),
  443. $e->getCode(),
  444. $e,
  445. );
  446. }
  447. // @codeCoverageIgnoreEnd
  448. }
  449. /**
  450. * @psalm-param callable-string $functionName
  451. *
  452. * @throws ReflectionException
  453. */
  454. private static function reflectorForFunction(string $functionName): ReflectionFunction
  455. {
  456. try {
  457. return new ReflectionFunction($functionName);
  458. // @codeCoverageIgnoreStart
  459. } catch (\ReflectionException $e) {
  460. throw new ReflectionException(
  461. $e->getMessage(),
  462. $e->getCode(),
  463. $e,
  464. );
  465. }
  466. // @codeCoverageIgnoreEnd
  467. }
  468. }