Annotations.test.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. <?php
  2. require_once __DIR__ . '/Annotations.case.php';
  3. require_once __DIR__ . '/Annotations.Sample.case.php';
  4. use mindplay\annotations\AnnotationFile;
  5. use mindplay\annotations\AnnotationCache;
  6. use mindplay\annotations\AnnotationManager;
  7. use mindplay\annotations\Annotations;
  8. use mindplay\annotations\Annotation;
  9. use mindplay\annotations\standard\ReturnAnnotation;
  10. use mindplay\test\annotations\Package;
  11. use mindplay\test\lib\xTest;
  12. use mindplay\test\lib\xTestRunner;
  13. if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
  14. require_once __DIR__ . '/traits/namespaced.php';
  15. require_once __DIR__ . '/traits/toplevel.php';
  16. }
  17. /**
  18. * This class implements tests for core annotations
  19. */
  20. class AnnotationsTest extends xTest
  21. {
  22. const ANNOTATION_EXCEPTION = 'mindplay\annotations\AnnotationException';
  23. /**
  24. * Run this test.
  25. *
  26. * @param xTestRunner $testRunner Test runner.
  27. * @return boolean
  28. */
  29. public function run(xTestRunner $testRunner)
  30. {
  31. $testRunner->startCoverageCollector(__CLASS__);
  32. $cachePath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'runtime';
  33. Annotations::$config = array(
  34. 'cache' => new AnnotationCache($cachePath),
  35. );
  36. if (!is_writable($cachePath)) {
  37. die('cache path is not writable: ' . $cachePath);
  38. }
  39. // manually wipe out the cache:
  40. $pattern = Annotations::getManager()->cache->getRoot() . DIRECTORY_SEPARATOR . '*.annotations.php';
  41. foreach (glob($pattern) as $path) {
  42. unlink($path);
  43. }
  44. // disable some annotations not used during testing:
  45. Annotations::getManager()->registry['var'] = false;
  46. Annotations::getManager()->registry['undefined'] = 'UndefinedAnnotation';
  47. $testRunner->stopCoverageCollector();
  48. return parent::run($testRunner);
  49. }
  50. protected function testCanResolveAnnotationNames()
  51. {
  52. $manager = new AnnotationManager;
  53. $manager->namespace = ''; // look for annotations in the global namespace
  54. $manager->suffix = 'Annotation'; // use a suffix for annotation class-names
  55. $this->check(
  56. $manager->resolveName('test') === 'TestAnnotation',
  57. 'should capitalize and suffix annotation names'
  58. );
  59. $this->check(
  60. $manager->resolveName('X\Y\Foo') === 'X\Y\FooAnnotation',
  61. 'should suffix fully qualified annotation names'
  62. );
  63. $manager->registry['test'] = 'X\Y\Z\TestAnnotation';
  64. $this->check(
  65. $manager->resolveName('test') === 'X\Y\Z\TestAnnotation',
  66. 'should respect registered annotation types'
  67. );
  68. $this->check(
  69. $manager->resolveName('Test') === 'X\Y\Z\TestAnnotation',
  70. 'should ignore case of first letter in annotation names'
  71. );
  72. $manager->registry['test'] = false;
  73. $this->check($manager->resolveName('test') === false, 'should respect disabled annotation types');
  74. $manager->namespace = 'ABC';
  75. $this->check($manager->resolveName('hello') === 'ABC\HelloAnnotation', 'should default to standard namespace');
  76. }
  77. protected function testCanGetAnnotationFile()
  78. {
  79. // This test is for an internal API, so we need to perform some invasive maneuvers:
  80. $manager = Annotations::getManager();
  81. $manager_reflection = new ReflectionClass($manager);
  82. $method = $manager_reflection->getMethod('getAnnotationFile');
  83. $method->setAccessible(true);
  84. $class_reflection = new ReflectionClass('mindplay\test\Sample\SampleClass');
  85. // absolute path to the class-file used for testing
  86. $file_path = $class_reflection->getFileName();
  87. // Now get the AnnotationFile instance:
  88. /** @var AnnotationFile $file */
  89. $file = $method->invoke($manager, $file_path);
  90. $this->check($file instanceof AnnotationFile, 'should be an instance of AnnotationFile');
  91. $this->check(count($file->data) > 0, 'should contain Annotation data');
  92. $this->check($file->path === $file_path, 'should reflect path to class-file');
  93. $this->check($file->namespace === 'mindplay\test\Sample', 'should reflect namespace');
  94. $this->check(
  95. $file->uses === array('Test' => 'Test', 'SampleAlias' => 'mindplay\annotations\Annotation'),
  96. 'should reflect use-clause'
  97. );
  98. }
  99. protected function testCanParseAnnotations()
  100. {
  101. $manager = new AnnotationManager;
  102. Package::register($manager);
  103. $manager->namespace = ''; // look for annotations in the global namespace
  104. $manager->suffix = 'Annotation'; // use a suffix for annotation class-names
  105. $parser = $manager->getParser();
  106. $source = "
  107. <?php
  108. namespace foo\\bar;
  109. use
  110. baz\\Hat as Zing,
  111. baz\\Zap;
  112. /**
  113. * @doc 123
  114. * @note('abc')
  115. * @required
  116. * @note('xyz');
  117. */
  118. class Sample {
  119. public function test()
  120. {
  121. \$var = null;
  122. \$test = function () use (\$var) {
  123. // this inline function is here to assert that the parser
  124. // won't pick up the use-clause of an inline function
  125. };
  126. }
  127. }
  128. ";
  129. $code = $parser->parse($source, 'inline-test');
  130. $test = eval($code);
  131. $this->check($test['#namespace'] === 'foo\bar', 'file namespace should be parsed and cached');
  132. $this->check(
  133. $test['#uses'] === array('Zing' => 'baz\Hat', 'Zap' => 'baz\Zap'),
  134. 'use-clauses should be parsed and cached: ' . var_export($test['#uses'], true)
  135. );
  136. $this->check($test['foo\bar\Sample'][0]['#name'] === 'doc', 'first annotation is an @doc annotation');
  137. $this->check($test['foo\bar\Sample'][0]['#type'] === 'DocAnnotation', 'first annotation is a DocAnnotation');
  138. $this->check($test['foo\bar\Sample'][0]['value'] === 123, 'first annotation has the value 123');
  139. $this->check($test['foo\bar\Sample'][1]['#name'] === 'note', 'second annotation is an @note annotation');
  140. $this->check($test['foo\bar\Sample'][1]['#type'] === 'NoteAnnotation', 'second annotation is a NoteAnnotation');
  141. $this->check($test['foo\bar\Sample'][1][0] === 'abc', 'value of second annotation is "abc"');
  142. $this->check(
  143. $test['foo\bar\Sample'][2]['#type'] === 'mindplay\test\annotations\RequiredAnnotation',
  144. 'third annotation is a RequiredAnnotation'
  145. );
  146. $this->check($test['foo\bar\Sample'][3]['#type'] === 'NoteAnnotation', 'last annotation is a NoteAnnotation');
  147. $this->check($test['foo\bar\Sample'][3][0] === 'xyz', 'value of last annotation is "xyz"');
  148. }
  149. protected function testCanGetStaticAnnotationManager()
  150. {
  151. if (Annotations::getManager() instanceof AnnotationManager) {
  152. $this->pass();
  153. } else {
  154. $this->fail();
  155. }
  156. }
  157. protected function testCanGetAnnotationUsage()
  158. {
  159. $usage = Annotations::getUsage('NoteAnnotation');
  160. $this->check($usage->class === true);
  161. $this->check($usage->property === true);
  162. $this->check($usage->method === true);
  163. $this->check($usage->inherited === true);
  164. $this->check($usage->multiple === true);
  165. }
  166. protected function testAnnotationWithNonUsageAndUsageAnnotations()
  167. {
  168. $this->setExpectedException(
  169. self::ANNOTATION_EXCEPTION,
  170. "The class 'UsageAndNonUsageAnnotation' must have exactly one UsageAnnotation (no other Annotations are allowed)"
  171. );
  172. Annotations::getUsage('UsageAndNonUsageAnnotation');
  173. }
  174. protected function testAnnotationWithSingleNonUsageAnnotation()
  175. {
  176. $this->setExpectedException(
  177. self::ANNOTATION_EXCEPTION,
  178. "The class 'SingleNonUsageAnnotation' must have exactly one UsageAnnotation (no other Annotations are allowed)"
  179. );
  180. Annotations::getUsage('SingleNonUsageAnnotation');
  181. }
  182. protected function testUsageAnnotationIsInherited()
  183. {
  184. $usage = Annotations::getUsage('InheritUsageAnnotation');
  185. $this->check($usage->method === true);
  186. }
  187. protected function testGetUsageOfUndefinedAnnotationClass()
  188. {
  189. $this->setExpectedException(
  190. self::ANNOTATION_EXCEPTION,
  191. "Annotation type 'NoSuchAnnotation' does not exist"
  192. );
  193. Annotations::getUsage('NoSuchAnnotation');
  194. }
  195. protected function testAnnotationWithoutUsageAnnotation()
  196. {
  197. $this->setExpectedException(
  198. self::ANNOTATION_EXCEPTION,
  199. "The class 'NoUsageAnnotation' must have exactly one UsageAnnotation"
  200. );
  201. Annotations::getUsage('NoUsageAnnotation');
  202. }
  203. protected function testCanGetClassAnnotations()
  204. {
  205. $annotations = Annotations::ofClass(new \ReflectionClass('Test'));
  206. $this->check(count($annotations) > 0, 'from class reflection');
  207. $annotations = Annotations::ofClass(new Test());
  208. $this->check(count($annotations) > 0, 'from class object');
  209. $annotations = Annotations::ofClass('Test');
  210. $this->check(count($annotations) > 0, 'from class name');
  211. }
  212. protected function testCanGetMethodAnnotations()
  213. {
  214. $annotations = Annotations::ofMethod(new \ReflectionClass('Test'), 'run');
  215. $this->check(count($annotations) > 0, 'from class reflection and method name');
  216. $annotations = Annotations::ofMethod(new \ReflectionMethod('Test', 'run'));
  217. $this->check(count($annotations) > 0, 'from method reflection');
  218. $annotations = Annotations::ofMethod(new Test(), 'run');
  219. $this->check(count($annotations) > 0, 'from class object and method name');
  220. $annotations = Annotations::ofMethod('Test', 'run');
  221. $this->check(count($annotations) > 0, 'from class name and method name');
  222. }
  223. protected function testGetAnnotationsFromMethodOfNonExistingClass()
  224. {
  225. $this->setExpectedException(
  226. self::ANNOTATION_EXCEPTION,
  227. "Unable to read annotations from an undefined class 'NonExistingClass'"
  228. );
  229. Annotations::ofMethod('NonExistingClass');
  230. }
  231. protected function testGetAnnotationsFromNonExistingMethodOfAClass()
  232. {
  233. $this->setExpectedException(
  234. self::ANNOTATION_EXCEPTION,
  235. 'Unable to read annotations from an undefined method Test::nonExistingMethod()'
  236. );
  237. Annotations::ofMethod('Test', 'nonExistingMethod');
  238. }
  239. protected function testCanGetPropertyAnnotations()
  240. {
  241. $annotations = Annotations::ofProperty(new \ReflectionClass('Test'), 'sample');
  242. $this->check(count($annotations) > 0, 'from class reflection and property name');
  243. $annotations = Annotations::ofProperty(new \ReflectionProperty('TestBase', 'sample'));
  244. $this->check(count($annotations) > 0, 'from property reflection');
  245. $annotations = Annotations::ofProperty(new Test(), 'sample');
  246. $this->check(count($annotations) > 0, 'from class object and property name');
  247. $annotations = Annotations::ofProperty('Test', 'sample');
  248. $this->check(count($annotations) > 0, 'from class name and property name');
  249. }
  250. protected function testGetAnnotationsFromPropertyOfNonExistingClass()
  251. {
  252. $this->setExpectedException(
  253. self::ANNOTATION_EXCEPTION,
  254. "Unable to read annotations from an undefined class 'NonExistingClass'"
  255. );
  256. Annotations::ofProperty('NonExistingClass', 'sample');
  257. }
  258. public function testGetAnnotationsFromNonExistingPropertyOfExistingClass()
  259. {
  260. $this->setExpectedException(
  261. self::ANNOTATION_EXCEPTION,
  262. 'Unable to read annotations from an undefined property Test::$nonExisting'
  263. );
  264. Annotations::ofProperty('Test', 'nonExisting');
  265. }
  266. protected function testCanGetFilteredClassAnnotations()
  267. {
  268. $anns = Annotations::ofClass('TestBase', 'NoteAnnotation');
  269. if (!count($anns)) {
  270. $this->fail('No annotations found');
  271. return;
  272. }
  273. foreach ($anns as $ann) {
  274. if (!$ann instanceof NoteAnnotation) {
  275. $this->fail();
  276. }
  277. }
  278. $this->pass();
  279. }
  280. protected function testCanGetFilteredMethodAnnotations()
  281. {
  282. $anns = Annotations::ofMethod('TestBase', 'run', 'NoteAnnotation');
  283. if (!count($anns)) {
  284. $this->fail('No annotations found');
  285. return;
  286. }
  287. foreach ($anns as $ann) {
  288. if (!$ann instanceof NoteAnnotation) {
  289. $this->fail();
  290. }
  291. }
  292. $this->pass();
  293. }
  294. protected function testCanGetFilteredPropertyAnnotations()
  295. {
  296. $anns = Annotations::ofProperty('Test', 'mixed', 'NoteAnnotation');
  297. if (!count($anns)) {
  298. $this->fail('No annotations found');
  299. return;
  300. }
  301. foreach ($anns as $ann) {
  302. if (!$ann instanceof NoteAnnotation) {
  303. $this->fail();
  304. }
  305. }
  306. $this->pass();
  307. }
  308. protected function testCanGetInheritedClassAnnotations()
  309. {
  310. $anns = Annotations::ofClass('Test');
  311. foreach ($anns as $ann) {
  312. if ($ann->note == 'Applied to the TestBase class') {
  313. $this->pass();
  314. return;
  315. }
  316. }
  317. $this->fail();
  318. }
  319. protected function testCanGetInheritedMethodAnnotations()
  320. {
  321. $anns = Annotations::ofMethod('Test', 'run');
  322. foreach ($anns as $ann) {
  323. if ($ann->note == 'Applied to a hidden TestBase method') {
  324. $this->pass();
  325. return;
  326. }
  327. }
  328. $this->fail();
  329. }
  330. protected function testCanGetInheritedPropertyAnnotations()
  331. {
  332. $anns = Annotations::ofProperty('Test', 'sample');
  333. foreach ($anns as $ann) {
  334. if ($ann->note == 'Applied to a TestBase member') {
  335. $this->pass();
  336. return;
  337. }
  338. }
  339. $this->fail();
  340. }
  341. protected function testDoesNotInheritUninheritableAnnotations()
  342. {
  343. $anns = Annotations::ofClass('Test');
  344. if (count($anns) == 0) {
  345. $this->fail();
  346. return;
  347. }
  348. foreach ($anns as $ann) {
  349. if ($ann instanceof UninheritableAnnotation) {
  350. $this->fail();
  351. return;
  352. }
  353. }
  354. $this->pass();
  355. }
  356. protected function testThrowsExceptionIfSingleAnnotationAppliedTwice()
  357. {
  358. $this->setExpectedException(
  359. self::ANNOTATION_EXCEPTION,
  360. "Only one annotation of 'SingleAnnotation' type may be applied to the same property"
  361. );
  362. Annotations::ofProperty('Test', 'only_one');
  363. }
  364. protected function testCanOverrideSingleAnnotation()
  365. {
  366. $anns = Annotations::ofProperty('Test', 'override_me');
  367. if (count($anns) != 1) {
  368. $this->fail(count($anns) . ' annotations found - expected 1');
  369. return;
  370. }
  371. $ann = reset($anns);
  372. if ($ann->test != 'This annotation overrides the one in TestBase') {
  373. $this->fail();
  374. } else {
  375. $this->pass();
  376. }
  377. }
  378. protected function testCanHandleEdgeCaseInParser()
  379. {
  380. // an edge-case was found in the parser - this test asserts that a php-doc style
  381. // annotation with no trailing characters after it will be parsed correctly.
  382. $anns = Annotations::ofClass('TestBase', 'DocAnnotation');
  383. $this->check(count($anns) == 1, 'one DocAnnotation was expected - found ' . count($anns));
  384. }
  385. protected function testCanHandleNamespaces()
  386. {
  387. // This test asserts that a namespaced class can be annotated, that annotations can
  388. // be namespaced, and that asking for annotations of a namespaced annotation-type
  389. // yields the expected result.
  390. $anns = Annotations::ofClass('mindplay\test\Sample\SampleClass', 'mindplay\test\Sample\SampleAnnotation');
  391. $this->check(count($anns) == 1, 'one SampleAnnotation was expected - found ' . count($anns));
  392. }
  393. protected function testCanUseAnnotationsInDefaultNamespace()
  394. {
  395. $manager = new AnnotationManager();
  396. $manager->namespace = 'mindplay\test\Sample';
  397. $manager->cache = false;
  398. $anns = $manager->getClassAnnotations(
  399. 'mindplay\test\Sample\AnnotationInDefaultNamespace',
  400. 'mindplay\test\Sample\SampleAnnotation'
  401. );
  402. $this->check(count($anns) == 1, 'one SampleAnnotation was expected - found ' . count($anns));
  403. }
  404. protected function testCanIgnoreAnnotations()
  405. {
  406. $manager = new AnnotationManager();
  407. $manager->namespace = 'mindplay\test\Sample';
  408. $manager->cache = false;
  409. $manager->registry['ignored'] = false;
  410. $anns = $manager->getClassAnnotations('mindplay\test\Sample\IgnoreMe');
  411. $this->check(count($anns) == 0, 'the @ignored annotation should be ignored');
  412. }
  413. protected function testCanUseAnnotationAlias()
  414. {
  415. $manager = new AnnotationManager();
  416. $manager->namespace = 'mindplay\test\Sample';
  417. $manager->cache = false;
  418. $manager->registry['aliased'] = 'mindplay\test\Sample\SampleAnnotation';
  419. /** @var Annotation[] $anns */
  420. $anns = $manager->getClassAnnotations('mindplay\test\Sample\AliasMe');
  421. $this->check(count($anns) == 1, 'the @aliased annotation should be aliased');
  422. $this->check(
  423. get_class($anns[0]) == 'mindplay\test\Sample\SampleAnnotation',
  424. 'returned @aliased annotation should map to mindplay\test\Sample\SampleAnnotation'
  425. );
  426. }
  427. protected function testCanFindAnnotationsByAlias()
  428. {
  429. $ann = Annotations::ofProperty('TestBase', 'sample', '@note');
  430. $this->check(count($ann) === 1, 'TestBase::$sample has one @note annotation');
  431. }
  432. protected function testParseUserDefinedClasses()
  433. {
  434. $annotations = Annotations::ofClass('TestClassExtendingUserDefined', '@note');
  435. $this->check(count($annotations) == 2, 'TestClassExtendingUserDefined has two note annotations.');
  436. }
  437. protected function testDoNotParseCoreClasses()
  438. {
  439. $annotations = Annotations::ofClass('TestClassExtendingCore', '@note');
  440. $this->check(count($annotations) == 1, 'TestClassExtendingCore has one note annotations.');
  441. }
  442. protected function testDoNotParseExtensionClasses()
  443. {
  444. $annotations = Annotations::ofClass('TestClassExtendingExtension', '@note');
  445. $this->check(count($annotations) == 1, 'TestClassExtendingExtension has one note annotations.');
  446. }
  447. protected function testGetAnnotationsFromNonExistingClass()
  448. {
  449. $this->setExpectedException(
  450. self::ANNOTATION_EXCEPTION,
  451. "Unable to read annotations from an undefined class/trait 'NonExistingClass'"
  452. );
  453. Annotations::ofClass('NonExistingClass', '@note');
  454. }
  455. protected function testGetAnnotationsFromAnInterface()
  456. {
  457. $this->setExpectedException(
  458. self::ANNOTATION_EXCEPTION,
  459. "Reading annotations from interface 'TestInterface' is not supported"
  460. );
  461. Annotations::ofClass('TestInterface', '@note');
  462. }
  463. protected function testGetAnnotationsFromTrait()
  464. {
  465. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  466. $this->pass();
  467. return;
  468. }
  469. $annotations = Annotations::ofClass('SimpleTrait', '@note');
  470. $this->check(count($annotations) === 1, 'SimpleTrait has one note annotation.');
  471. }
  472. protected function testCanGetMethodAnnotationsIncludedFromTrait()
  473. {
  474. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  475. $this->pass();
  476. return;
  477. }
  478. $annotations = Annotations::ofMethod('SimpleTraitTester', 'runFromTrait');
  479. $this->check(count($annotations) > 0, 'for unnamespaced trait');
  480. $annotations = Annotations::ofMethod('SimpleTraitTester', 'runFromAnotherTrait');
  481. $this->check(count($annotations) > 0, 'for namespaced trait');
  482. }
  483. protected function testHandlesMethodInheritanceWithTraits()
  484. {
  485. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  486. $this->pass();
  487. return;
  488. }
  489. $annotations = Annotations::ofMethod('InheritanceTraitTester', 'baseTraitAndParent');
  490. $this->check(count($annotations) === 2, 'baseTraitAndParent inherits parent annotations');
  491. $this->check($annotations[0]->note === 'inheritance-base-trait-tester', 'parent annotation first');
  492. $this->check($annotations[1]->note === 'inheritance-base-trait', 'trait annotation second');
  493. $annotations = Annotations::ofMethod('InheritanceTraitTester', 'traitAndParent');
  494. $this->check(count($annotations) === 2, 'traitAndParent inherits parent annotations');
  495. $this->check($annotations[0]->note === 'inheritance-base-trait-tester', 'parent annotation first');
  496. $this->check($annotations[1]->note === 'inheritance-trait', 'trait annotation second');
  497. $annotations = Annotations::ofMethod('InheritanceTraitTester', 'traitAndChild');
  498. $this->check(count($annotations) === 1, 'traitAndChild does not inherit trait');
  499. $this->check($annotations[0]->note === 'inheritance-trait-tester', 'child annotation first');
  500. $annotations = Annotations::ofMethod('InheritanceTraitTester', 'traitAndParentAndChild');
  501. $this->check(count($annotations) === 2, 'traitAndParentAndChild does not inherit trait annotation');
  502. $this->check($annotations[0]->note === 'inheritance-base-trait-tester', 'parent annotation first');
  503. $this->check($annotations[1]->note === 'inheritance-trait-tester', 'child annotation second');
  504. }
  505. protected function testHandlesMethodAliasingWithTraits()
  506. {
  507. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  508. $this->pass();
  509. return;
  510. }
  511. $annotations = Annotations::ofMethod('AliasTraitTester', 'baseTraitRun');
  512. $this->check(count($annotations) === 2, 'baseTraitRun inherits annotation');
  513. $this->check($annotations[0]->note === 'alias-base-trait-tester', 'inherited annotation goes first');
  514. $this->check($annotations[1]->note === 'alias-base-trait', 'non-inherited annotation goes second');
  515. $annotations = Annotations::ofMethod('AliasTraitTester', 'traitRun');
  516. $this->check(count($annotations) === 2, 'traitRun inherits annotation');
  517. $this->check($annotations[0]->note === 'alias-base-trait-tester', 'inherited annotation goes first');
  518. $this->check($annotations[1]->note === 'alias-trait', 'non-inherited annotation goes second');
  519. $annotations = Annotations::ofMethod('AliasTraitTester', 'run');
  520. $this->check(count($annotations) === 2, 'run inherits annotation');
  521. $this->check($annotations[0]->note === 'alias-base-trait-tester', 'inherited annotation goes first');
  522. $this->check($annotations[1]->note === 'alias-trait-tester', 'non-inherited annotation goes second');
  523. }
  524. protected function testHandlesConflictedMethodSelectionWithTraits()
  525. {
  526. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  527. $this->pass();
  528. return;
  529. }
  530. $annotations = Annotations::ofMethod('InsteadofTraitTester', 'baseTrait');
  531. $this->check(count($annotations) === 2, 'baseTrait inherits annotation');
  532. $this->check($annotations[0]->note === 'insteadof-base-trait-tester', 'inherited annotation goes first');
  533. $this->check($annotations[1]->note === 'insteadof-base-trait-b', 'non-inherited annotation goes second');
  534. $annotations = Annotations::ofMethod('InsteadofTraitTester', 'trate');
  535. $this->check(count($annotations) === 2, 'trate inherits annotation');
  536. $this->check($annotations[0]->note === 'insteadof-base-trait-tester', 'inherited annotation goes first');
  537. $this->check($annotations[1]->note === 'insteadof-trait-a', 'non-inherited annotation goes second');
  538. }
  539. protected function testCanGetPropertyAnnotationsIncludedFromTrait()
  540. {
  541. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  542. $this->pass();
  543. return;
  544. }
  545. $annotations = Annotations::ofProperty('SimpleTraitTester', 'sampleFromTrait');
  546. $this->check(count($annotations) > 0, 'for unnamespaced trait');
  547. $annotations = Annotations::ofProperty('SimpleTraitTester', 'sampleFromAnotherTrait');
  548. $this->check(count($annotations) > 0, 'for namespaced trait');
  549. }
  550. protected function testHandlesPropertyConflictWithTraits()
  551. {
  552. if (version_compare(PHP_VERSION, '5.4.0', '<')) {
  553. $this->pass();
  554. return;
  555. }
  556. set_error_handler(function ($errno, $errstring) { });
  557. require_once __DIR__ . '/traits/property_conflict.php';
  558. restore_error_handler();
  559. $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'traitAndChild');
  560. $this->check(count($annotations) === 1, 'traitAndChild does not inherit trait');
  561. $this->check($annotations[0]->note === 'property-conflict-trait-tester', 'child annotation first');
  562. $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'traitAndTraitAndParent');
  563. $this->check(count($annotations) === 2, 'traitAndTraitAndParent inherits parent annotations');
  564. $this->check($annotations[0]->note === 'property-conflict-base-trait-tester', 'parent annotation first');
  565. $this->check($annotations[1]->note === 'property-conflict-trait-two', 'first listed trait annotation second');
  566. $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'unannotatedTraitAndAnnotatedTrait');
  567. $this->check(count($annotations) === 0, 'unannotatedTraitAndAnnotatedTrait has no annotations');
  568. $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'traitAndParentAndChild');
  569. $this->check(count($annotations) === 2, 'traitAndParentAndChild does not inherit trait annotation');
  570. $this->check($annotations[0]->note === 'property-conflict-base-trait-tester', 'parent annotation first');
  571. $this->check($annotations[1]->note === 'property-conflict-trait-tester', 'child annotation second');
  572. }
  573. protected function testDisallowReadingUndefinedAnnotationProperties()
  574. {
  575. $nodeAnnotation = new NoteAnnotation();
  576. $this->setExpectedException(
  577. self::ANNOTATION_EXCEPTION,
  578. 'NoteAnnotation::$nonExisting is not a valid property name'
  579. );
  580. $result = $nodeAnnotation->nonExisting;
  581. }
  582. protected function testDisallowWritingUndefinedAnnotationProperties()
  583. {
  584. $nodeAnnotation = new NoteAnnotation();
  585. $this->setExpectedException(
  586. self::ANNOTATION_EXCEPTION,
  587. 'NoteAnnotation::$nonExisting is not a valid property name'
  588. );
  589. $nodeAnnotation->nonExisting = 'new value';
  590. }
  591. protected function testAnnotationCacheGetTimestamp()
  592. {
  593. $annotationCache = new AnnotationCache(sys_get_temp_dir());
  594. $annotationCache->store('sample', '');
  595. $this->check(
  596. $annotationCache->getTimestamp('sample') > strtotime('midnight'),
  597. 'Annotation cache last update timestamp is not stale'
  598. );
  599. }
  600. protected function testAnnotationFileTypeResolution()
  601. {
  602. $annotationFile = new AnnotationFile('', array(
  603. '#namespace' => 'LevelA\NS',
  604. '#uses' => array(
  605. 'LevelBClass' => 'LevelB\Class',
  606. ),
  607. ));
  608. $this->check(
  609. $annotationFile->resolveType('SubNS1\SubClass') == 'LevelA\NS\SubNS1\SubClass',
  610. 'Class in sub-namespace is resolved correctly'
  611. );
  612. $this->check(
  613. $annotationFile->resolveType('\SubNS1\SubClass') == '\SubNS1\SubClass',
  614. 'Fully qualified class name is not changed during resolution'
  615. );
  616. $this->check(
  617. $annotationFile->resolveType('LevelBClass') == 'LevelB\Class',
  618. 'The "uses ..." clause (exact match) is being used during resolution'
  619. );
  620. $this->check(
  621. $annotationFile->resolveType('SomeClass[]') == 'LevelA\NS\SomeClass[]',
  622. 'The [] at then end of data type are preserved during resolution'
  623. );
  624. $this->check(
  625. $annotationFile->resolveType('integer') == 'integer',
  626. 'Simple data type is kept as-is during resolution'
  627. );
  628. }
  629. protected function testAnnotationManagerWithoutCache()
  630. {
  631. $this->setExpectedException(
  632. self::ANNOTATION_EXCEPTION,
  633. 'AnnotationManager::$cache is not configured'
  634. );
  635. $annotationManager = new AnnotationManager();
  636. $annotationManager->getClassAnnotations('NoteAnnotation');
  637. }
  638. protected function testUsingNonExistentAnnotation()
  639. {
  640. $this->setExpectedException(
  641. self::ANNOTATION_EXCEPTION,
  642. "Annotation type 'WrongInterfaceAnnotation' does not implement the mandatory IAnnotation interface"
  643. );
  644. Annotations::ofClass('TestClassWrongInterface');
  645. }
  646. protected function testReadingFileAwareAnnotation()
  647. {
  648. $annotations = Annotations::ofProperty('TestClassFileAwareAnnotation', 'prop', '@TypeAware');
  649. $this->check(count($annotations) == 1, 'the @TypeAware annotation was found');
  650. $this->check(
  651. $annotations[0]->type == 'mindplay\annotations\IAnnotationParser',
  652. 'data type of type-aware annotation was resolved'
  653. );
  654. }
  655. protected function testAnnotationsConstrainedByCorrectUsageAnnotation()
  656. {
  657. $annotations = array(new NoteAnnotation());
  658. $this->assertApplyConstrains($annotations, 'class');
  659. }
  660. protected function testAnnotationsConstrainedByClass()
  661. {
  662. $annotations = array(new UselessAnnotation());
  663. $this->setExpectedException(
  664. self::ANNOTATION_EXCEPTION,
  665. "Annotation type 'UselessAnnotation' cannot be applied to a class"
  666. );
  667. $this->assertApplyConstrains($annotations, AnnotationManager::MEMBER_CLASS);
  668. }
  669. protected function testAnnotationsConstrainedByMethod()
  670. {
  671. $annotations = array(new UselessAnnotation());
  672. $this->setExpectedException(
  673. self::ANNOTATION_EXCEPTION,
  674. "Annotation type 'UselessAnnotation' cannot be applied to a method"
  675. );
  676. $this->assertApplyConstrains($annotations, AnnotationManager::MEMBER_METHOD);
  677. }
  678. protected function testAnnotationsConstrainedByProperty()
  679. {
  680. $annotations = array(new UselessAnnotation());
  681. $this->setExpectedException(
  682. self::ANNOTATION_EXCEPTION,
  683. "Annotation type 'UselessAnnotation' cannot be applied to a property"
  684. );
  685. $this->assertApplyConstrains($annotations, AnnotationManager::MEMBER_PROPERTY);
  686. }
  687. protected function assertApplyConstrains(array &$annotations, $memberType)
  688. {
  689. $manager = Annotations::getManager();
  690. $methodReflection = new ReflectionMethod(get_class($manager), 'applyConstraints');
  691. $methodReflection->setAccessible(true);
  692. $methodReflection->invokeArgs($manager, array(&$annotations, $memberType));
  693. $this->check(count($annotations) > 0);
  694. }
  695. public function testStopAnnotationPreventsClassLevelAnnotationInheritance()
  696. {
  697. $annotations = Annotations::ofClass('SecondClass', '@note');
  698. $this->check(count($annotations) === 1, 'class level annotation after own "@stop" not present');
  699. $this->check($annotations[0]->note === 'class-second', 'non-inherited annotation goes first');
  700. $annotations = Annotations::ofClass('ThirdClass', '@note');
  701. $this->check(count($annotations) === 2, 'class level annotation after parent "@stop" not present');
  702. $this->check($annotations[0]->note === 'class-second', 'inherited annotation goes first');
  703. $this->check($annotations[1]->note === 'class-third', 'non-inherited annotation goes second');
  704. }
  705. public function testStopAnnotationPreventsPropertyLevelAnnotationInheritance()
  706. {
  707. $annotations = Annotations::ofProperty('SecondClass', 'prop', '@note');
  708. $this->check(count($annotations) === 1, 'property level annotation after own "@stop" not present');
  709. $this->check($annotations[0]->note === 'prop-second', 'non-inherited annotation goes first');
  710. $annotations = Annotations::ofProperty('ThirdClass', 'prop', '@note');
  711. $this->check(count($annotations) === 2, 'property level annotation after parent "@stop" not present');
  712. $this->check($annotations[0]->note === 'prop-second', 'inherited annotation goes first');
  713. $this->check($annotations[1]->note === 'prop-third', 'non-inherited annotation goes second');
  714. }
  715. public function testStopAnnotationPreventsMethodLevelAnnotationInheritance()
  716. {
  717. $annotations = Annotations::ofMethod('SecondClass', 'someMethod', '@note');
  718. $this->check(count($annotations) === 1, 'method level annotation after own "@stop" not present');
  719. $this->check($annotations[0]->note === 'method-second', 'non-inherited annotation goes first');
  720. $annotations = Annotations::ofMethod('ThirdClass', 'someMethod', '@note');
  721. $this->check(count($annotations) === 2, 'method level annotation after parent "@stop" not present');
  722. $this->check($annotations[0]->note === 'method-second', 'inherited annotation goes first');
  723. $this->check($annotations[1]->note === 'method-third', 'non-inherited annotation goes second');
  724. }
  725. public function testDirectAccessToClassIgnoresStopAnnotation()
  726. {
  727. $annotations = Annotations::ofClass('FirstClass', '@note');
  728. $this->check(count($annotations) === 1);
  729. $this->check($annotations[0]->note === 'class-first');
  730. $annotations = Annotations::ofProperty('FirstClass', 'prop', '@note');
  731. $this->check(count($annotations) === 1);
  732. $this->check($annotations[0]->note === 'prop-first');
  733. $annotations = Annotations::ofMethod('FirstClass', 'someMethod', '@note');
  734. $this->check(count($annotations) === 1);
  735. $this->check($annotations[0]->note === 'method-first');
  736. }
  737. protected function testFilterUnresolvedAnnotationClass()
  738. {
  739. $annotations = Annotations::ofClass('TestBase', false);
  740. $this->check($annotations === array(), 'empty annotation list when filtering failed');
  741. }
  742. public function testMalformedParamAnnotationThrowsException()
  743. {
  744. $this->setExpectedException(
  745. self::ANNOTATION_EXCEPTION,
  746. 'ParamAnnotation requires a type property'
  747. );
  748. Annotations::ofMethod('BrokenParamAnnotationClass', 'brokenParamAnnotation');
  749. }
  750. protected function testOrphanedAnnotationsAreIgnored()
  751. {
  752. $manager = new AnnotationManager();
  753. $manager->namespace = 'mindplay\test\Sample';
  754. $manager->cache = false;
  755. /** @var Annotation[] $annotations */
  756. $annotations = $manager->getMethodAnnotations('mindplay\test\Sample\OrphanedAnnotations', 'someMethod');
  757. $this->check(count($annotations) == 1, 'the @return annotation was found');
  758. $this->check(
  759. $annotations[0] instanceof ReturnAnnotation,
  760. 'the @return annotation has correct type'
  761. );
  762. }
  763. }
  764. return new AnnotationsTest;