CodeUnit.php 12 KB

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