FunctionsAnalyzerTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Tests\Tokenizer\Analyzer;
  13. use PhpCsFixer\Tests\TestCase;
  14. use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
  15. use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
  16. use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
  17. use PhpCsFixer\Tokenizer\Tokens;
  18. /**
  19. * @author VeeWee <toonverwerft@gmail.com>
  20. *
  21. * @internal
  22. *
  23. * @covers \PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer
  24. */
  25. final class FunctionsAnalyzerTest extends TestCase
  26. {
  27. /**
  28. * @param list<int> $indices
  29. *
  30. * @dataProvider provideIsGlobalFunctionCallCases
  31. */
  32. public function testIsGlobalFunctionCall(string $code, array $indices): void
  33. {
  34. self::assertIsGlobalFunctionCall($indices, $code);
  35. }
  36. /**
  37. * @return iterable<array{string, list<int>}>
  38. */
  39. public static function provideIsGlobalFunctionCallCases(): iterable
  40. {
  41. yield [
  42. '<?php CONSTANT;',
  43. [],
  44. ];
  45. yield [
  46. '<?php foo();',
  47. [1],
  48. ];
  49. yield [
  50. '<?php foo("bar");',
  51. [1],
  52. ];
  53. yield [
  54. '<?php \foo("bar");',
  55. [2],
  56. ];
  57. yield [
  58. '<?php foo\bar("baz");',
  59. [],
  60. ];
  61. yield [
  62. '<?php foo::bar("baz");',
  63. [],
  64. ];
  65. yield [
  66. '<?php $foo->bar("baz");',
  67. [],
  68. ];
  69. yield [
  70. '<?php new bar("baz");',
  71. [],
  72. ];
  73. yield [
  74. '<?php function foo() {}',
  75. [],
  76. ];
  77. yield 'function with ref. return' => [
  78. '<?php function & foo() {}',
  79. [],
  80. ];
  81. yield [
  82. '<?php namespace\foo("bar");',
  83. [],
  84. ];
  85. yield [
  86. '<?php
  87. namespace A {
  88. use function A;
  89. }
  90. namespace B {
  91. use function D;
  92. A();
  93. }
  94. ',
  95. [30],
  96. ];
  97. yield [
  98. '<?php
  99. function A(){}
  100. A();
  101. ',
  102. [10],
  103. ];
  104. yield [
  105. '<?php
  106. function A(){}
  107. a();
  108. ',
  109. [10],
  110. ];
  111. yield [
  112. '<?php
  113. namespace {
  114. function A(){}
  115. A();
  116. }
  117. ',
  118. [14],
  119. ];
  120. yield [
  121. '<?php
  122. namespace Z {
  123. function A(){}
  124. A();
  125. }
  126. ',
  127. [],
  128. ];
  129. yield [
  130. '<?php
  131. namespace Z;
  132. function A(){}
  133. A();
  134. ',
  135. [],
  136. ];
  137. yield 'function signature ref. return, calls itself' => [
  138. '<?php
  139. function & A(){}
  140. A();
  141. ',
  142. [12],
  143. ];
  144. yield [
  145. '<?php
  146. class Foo
  147. {
  148. public function A(){}
  149. }
  150. A();
  151. ',
  152. [20],
  153. ];
  154. yield [
  155. '<?php
  156. namespace A {
  157. function A(){}
  158. }
  159. namespace B {
  160. A();
  161. }
  162. ',
  163. [24],
  164. ];
  165. yield [
  166. '<?php
  167. use function X\a;
  168. A();
  169. ',
  170. [],
  171. ];
  172. yield [
  173. '<?php
  174. use A;
  175. A();
  176. ',
  177. [7],
  178. ];
  179. yield [
  180. '<?php
  181. use const A;
  182. A();
  183. ',
  184. [9],
  185. ];
  186. yield [
  187. '<?php
  188. use function A;
  189. str_repeat($a, $b);
  190. ',
  191. [9],
  192. ];
  193. yield [
  194. '<?php
  195. namespace {
  196. function A(){}
  197. A();
  198. $b = function(){};
  199. }
  200. ',
  201. [14],
  202. ];
  203. yield [
  204. '<?php implode($a);implode($a);implode($a);implode($a);implode($a);implode($a);',
  205. [1, 6, 11, 16, 21, 26],
  206. ];
  207. yield [
  208. '<?php
  209. $z = new class(
  210. new class(){ private function A(){} }
  211. ){
  212. public function A() {}
  213. };
  214. A();
  215. ',
  216. [46],
  217. ];
  218. yield [
  219. '<?php $foo = fn() => false;',
  220. [],
  221. ];
  222. yield [
  223. '<?php foo("bar"); class A { function Foo(){ foo(); } }',
  224. [1, 20],
  225. ];
  226. }
  227. /**
  228. * @param list<int> $indices
  229. *
  230. * @dataProvider provideIsGlobalFunctionCallPre80Cases
  231. *
  232. * @requires PHP <8.0
  233. */
  234. public function testIsGlobalFunctionCallPre80(string $code, array $indices): void
  235. {
  236. self::assertIsGlobalFunctionCall($indices, $code);
  237. }
  238. /**
  239. * @return iterable<array{string, list<int>}>
  240. */
  241. public static function provideIsGlobalFunctionCallPre80Cases(): iterable
  242. {
  243. yield [
  244. '<?php
  245. use function \ str_repeat;
  246. str_repeat($a, $b);
  247. ',
  248. [11],
  249. ];
  250. }
  251. /**
  252. * @param list<int> $indices
  253. *
  254. * @dataProvider provideIsGlobalFunctionCallPhp80Cases
  255. *
  256. * @requires PHP 8.0
  257. */
  258. public function testIsGlobalFunctionCallPhp80(string $code, array $indices): void
  259. {
  260. self::assertIsGlobalFunctionCall($indices, $code);
  261. }
  262. public static function provideIsGlobalFunctionCallPhp80Cases(): iterable
  263. {
  264. yield [
  265. '<?php $a = new (foo());',
  266. [8],
  267. ];
  268. yield [
  269. '<?php $b = $foo instanceof (foo());',
  270. [10],
  271. ];
  272. yield [
  273. '<?php
  274. #[\Attribute(\Attribute::TARGET_CLASS)]
  275. class Foo {}
  276. ',
  277. [],
  278. ];
  279. yield [
  280. '<?php $x?->count();',
  281. [],
  282. ];
  283. yield [
  284. '<?php
  285. #[Foo(), Bar(), Baz()]
  286. class Foo {}
  287. ',
  288. [],
  289. ];
  290. }
  291. /**
  292. * @param list<int> $indices
  293. *
  294. * @dataProvider provideIsGlobalFunctionCallPhp81Cases
  295. *
  296. * @requires PHP 8.1
  297. */
  298. public function testIsGlobalFunctionCallPhp81(array $indices, string $code): void
  299. {
  300. self::assertIsGlobalFunctionCall($indices, $code);
  301. }
  302. public static function provideIsGlobalFunctionCallPhp81Cases(): iterable
  303. {
  304. yield 'first class callable cases' => [
  305. [],
  306. '<?php
  307. strlen(...);
  308. \strlen(...);
  309. $closure(...);
  310. $invokableObject(...);
  311. $obj->method(...);
  312. $obj->$methodStr(...);
  313. ($obj->property)(...);
  314. Foo::method(...);
  315. $classStr::$methodStr(...);
  316. self::{$complex . $expression}(...);
  317. \'strlen\'(...);
  318. [$obj, \'method\'](...);
  319. [Foo::class, \'method\'](...);
  320. $c = new class{};
  321. $b = new class(){};
  322. $a = new #[foo]
  323. class(){};
  324. ',
  325. ];
  326. yield [
  327. [1, 20],
  328. '<?php foo("bar"); enum A { function Foo(){ foo(); } }',
  329. ];
  330. }
  331. /**
  332. * @param array<string, ArgumentAnalysis> $expected
  333. *
  334. * @dataProvider provideFunctionArgumentInfoCases
  335. */
  336. public function testFunctionArgumentInfo(string $code, int $methodIndex, array $expected): void
  337. {
  338. $tokens = Tokens::fromCode($code);
  339. $analyzer = new FunctionsAnalyzer();
  340. self::assertSame(serialize($expected), serialize($analyzer->getFunctionArguments($tokens, $methodIndex)));
  341. }
  342. /**
  343. * @return iterable<array{string, int, array<string, ArgumentAnalysis>}>
  344. */
  345. public static function provideFunctionArgumentInfoCases(): iterable
  346. {
  347. yield ['<?php function(){};', 1, []];
  348. yield ['<?php function($a){};', 1, [
  349. '$a' => new ArgumentAnalysis(
  350. '$a',
  351. 3,
  352. null,
  353. null
  354. ),
  355. ]];
  356. yield ['<?php function($a, $b){};', 1, [
  357. '$a' => new ArgumentAnalysis(
  358. '$a',
  359. 3,
  360. null,
  361. null
  362. ),
  363. '$b' => new ArgumentAnalysis(
  364. '$b',
  365. 6,
  366. null,
  367. null
  368. ),
  369. ]];
  370. yield ['<?php function($a, $b = array(1,2), $c = 3){};', 1, [
  371. '$a' => new ArgumentAnalysis(
  372. '$a',
  373. 3,
  374. null,
  375. null
  376. ),
  377. '$b' => new ArgumentAnalysis(
  378. '$b',
  379. 6,
  380. 'array(1,2)',
  381. null
  382. ),
  383. '$c' => new ArgumentAnalysis(
  384. '$c',
  385. 18,
  386. '3',
  387. null
  388. ),
  389. ]];
  390. yield ['<?php function(array $a = array()){};', 1, [
  391. '$a' => new ArgumentAnalysis(
  392. '$a',
  393. 5,
  394. 'array()',
  395. new TypeAnalysis(
  396. 'array',
  397. 3,
  398. 3
  399. )
  400. ),
  401. ]];
  402. yield ['<?php function(array ... $a){};', 1, [
  403. '$a' => new ArgumentAnalysis(
  404. '$a',
  405. 7,
  406. null,
  407. new TypeAnalysis(
  408. 'array',
  409. 3,
  410. 3
  411. )
  412. ),
  413. ]];
  414. yield ['<?php function(\Foo\Bar $a){};', 1, [
  415. '$a' => new ArgumentAnalysis(
  416. '$a',
  417. 8,
  418. null,
  419. new TypeAnalysis(
  420. '\Foo\Bar',
  421. 3,
  422. 6
  423. )
  424. ),
  425. ]];
  426. yield ['<?php fn() => null;', 1, []];
  427. yield ['<?php fn($a) => null;', 1, [
  428. '$a' => new ArgumentAnalysis(
  429. '$a',
  430. 3,
  431. null,
  432. null
  433. ),
  434. ]];
  435. yield ['<?php fn($a, $b) => null;', 1, [
  436. '$a' => new ArgumentAnalysis(
  437. '$a',
  438. 3,
  439. null,
  440. null
  441. ),
  442. '$b' => new ArgumentAnalysis(
  443. '$b',
  444. 6,
  445. null,
  446. null
  447. ),
  448. ]];
  449. yield ['<?php fn($a, $b = array(1,2), $c = 3) => null;', 1, [
  450. '$a' => new ArgumentAnalysis(
  451. '$a',
  452. 3,
  453. null,
  454. null
  455. ),
  456. '$b' => new ArgumentAnalysis(
  457. '$b',
  458. 6,
  459. 'array(1,2)',
  460. null
  461. ),
  462. '$c' => new ArgumentAnalysis(
  463. '$c',
  464. 18,
  465. '3',
  466. null
  467. ),
  468. ]];
  469. yield ['<?php fn(array $a = array()) => null;', 1, [
  470. '$a' => new ArgumentAnalysis(
  471. '$a',
  472. 5,
  473. 'array()',
  474. new TypeAnalysis(
  475. 'array',
  476. 3,
  477. 3
  478. )
  479. ),
  480. ]];
  481. yield ['<?php fn(array ... $a) => null;', 1, [
  482. '$a' => new ArgumentAnalysis(
  483. '$a',
  484. 7,
  485. null,
  486. new TypeAnalysis(
  487. 'array',
  488. 3,
  489. 3
  490. )
  491. ),
  492. ]];
  493. yield ['<?php fn(\Foo\Bar $a) => null;', 1, [
  494. '$a' => new ArgumentAnalysis(
  495. '$a',
  496. 8,
  497. null,
  498. new TypeAnalysis(
  499. '\Foo\Bar',
  500. 3,
  501. 6
  502. )
  503. ),
  504. ]];
  505. }
  506. /**
  507. * @param array<string, ArgumentAnalysis> $expected
  508. *
  509. * @dataProvider provideFunctionArgumentInfoPre80Cases
  510. *
  511. * @requires PHP <8.0
  512. */
  513. public function testFunctionArgumentInfoPre80(string $code, int $methodIndex, array $expected): void
  514. {
  515. $this->testFunctionArgumentInfo($code, $methodIndex, $expected);
  516. }
  517. /**
  518. * @return iterable<array{string, int, array<string, ArgumentAnalysis>}>
  519. */
  520. public static function provideFunctionArgumentInfoPre80Cases(): iterable
  521. {
  522. yield ['<?php fn(\Foo/** TODO: change to something else */\Bar $a) => null;', 1, [
  523. '$a' => new ArgumentAnalysis(
  524. '$a',
  525. 9,
  526. null,
  527. new TypeAnalysis(
  528. '\Foo\Bar',
  529. 3,
  530. 7
  531. )
  532. ),
  533. ]];
  534. yield ['<?php function(\Foo/** TODO: change to something else */\Bar $a){};', 1, [
  535. '$a' => new ArgumentAnalysis(
  536. '$a',
  537. 9,
  538. null,
  539. new TypeAnalysis(
  540. '\Foo\Bar',
  541. 3,
  542. 7
  543. )
  544. ),
  545. ]];
  546. }
  547. /**
  548. * @dataProvider provideFunctionReturnTypeInfoCases
  549. */
  550. public function testFunctionReturnTypeInfo(string $code, int $methodIndex, ?TypeAnalysis $expected): void
  551. {
  552. $tokens = Tokens::fromCode($code);
  553. $analyzer = new FunctionsAnalyzer();
  554. $actual = $analyzer->getFunctionReturnType($tokens, $methodIndex);
  555. self::assertSame(serialize($expected), serialize($actual));
  556. }
  557. /**
  558. * @return iterable<array{string, int, null|TypeAnalysis}>
  559. */
  560. public static function provideFunctionReturnTypeInfoCases(): iterable
  561. {
  562. yield ['<?php function(){};', 1, null];
  563. yield ['<?php function($a): array {};', 1, new TypeAnalysis('array', 7, 7)];
  564. yield ['<?php function($a): \Foo\Bar {};', 1, new TypeAnalysis('\Foo\Bar', 7, 10)];
  565. yield ['<?php function($a): /* not sure if really an array */array {};', 1, new TypeAnalysis('array', 8, 8)];
  566. yield ['<?php fn() => null;', 1, null];
  567. yield ['<?php fn(array $a) => null;', 1, null];
  568. yield ['<?php fn($a): array => null;', 1, new TypeAnalysis('array', 7, 7)];
  569. yield ['<?php fn($a): \Foo\Bar => null;', 1, new TypeAnalysis('\Foo\Bar', 7, 10)];
  570. yield ['<?php fn($a): /* not sure if really an array */array => null;', 1, new TypeAnalysis('array', 8, 8)];
  571. }
  572. /**
  573. * @dataProvider provideFunctionReturnTypeInfoPre80Cases
  574. *
  575. * @requires PHP <8.0
  576. */
  577. public function testFunctionReturnTypeInfoPre80(string $code, int $methodIndex, ?TypeAnalysis $expected): void
  578. {
  579. $this->testFunctionReturnTypeInfo($code, $methodIndex, $expected);
  580. }
  581. /**
  582. * @return iterable<array{string, int, null|TypeAnalysis}>
  583. */
  584. public static function provideFunctionReturnTypeInfoPre80Cases(): iterable
  585. {
  586. yield ['<?php function($a): \Foo/** TODO: change to something else */\Bar {};', 1, new TypeAnalysis('\Foo\Bar', 7, 11)];
  587. yield ['<?php fn($a): \Foo/** TODO: change to something else */\Bar => null;', 1, new TypeAnalysis('\Foo\Bar', 7, 11)];
  588. }
  589. public function testIsTheSameClassCallInvalidIndex(): void
  590. {
  591. $tokens = Tokens::fromCode('<?php 1;2;');
  592. $analyzer = new FunctionsAnalyzer();
  593. $this->expectException(\InvalidArgumentException::class);
  594. $this->expectExceptionMessage('Token index 666 does not exist.');
  595. $analyzer->isTheSameClassCall($tokens, 666);
  596. }
  597. /**
  598. * @dataProvider provideIsTheSameClassCallCases
  599. *
  600. * @param list<int> $sameClassCallIndices
  601. */
  602. public function testIsTheSameClassCall(string $code, array $sameClassCallIndices): void
  603. {
  604. $tokens = Tokens::fromCode($code);
  605. $analyzer = new FunctionsAnalyzer();
  606. for ($index = $tokens->count() - 1; $index >= 0; --$index) {
  607. self::assertSame(
  608. \in_array($index, $sameClassCallIndices, true),
  609. $analyzer->isTheSameClassCall($tokens, $index),
  610. \sprintf('Index %d failed check.', $index)
  611. );
  612. }
  613. }
  614. /**
  615. * @return iterable<array{string, list<int>}>
  616. */
  617. public static function provideIsTheSameClassCallCases(): iterable
  618. {
  619. $template = '<?php
  620. class Foo {
  621. public function methodOne() {
  622. $x = %sotherMethod(1, 2, 3);
  623. }
  624. }
  625. ';
  626. yield [
  627. \sprintf($template, '$this->'),
  628. [24],
  629. ];
  630. yield [
  631. \sprintf($template, 'self::'),
  632. [24],
  633. ];
  634. yield [
  635. \sprintf($template, 'static::'),
  636. [24],
  637. ];
  638. yield [
  639. \sprintf($template, '$THIS->'),
  640. [24],
  641. ];
  642. yield [
  643. \sprintf($template, '$notThis->'),
  644. [],
  645. ];
  646. yield [
  647. \sprintf($template, 'Bar::'),
  648. [],
  649. ];
  650. yield [
  651. \sprintf($template, '$this::'),
  652. [24],
  653. ];
  654. yield [
  655. <<<'PHP'
  656. <?php
  657. class Foo {
  658. private $bar;
  659. public function bar() {
  660. return $this->bar;
  661. }
  662. }
  663. PHP,
  664. [],
  665. ];
  666. }
  667. /**
  668. * @dataProvider provideIsTheSameClassCall80Cases
  669. *
  670. * @param list<int> $sameClassCallIndices
  671. *
  672. * @requires PHP 8.0
  673. */
  674. public function testIsTheSameClassCall80(string $code, array $sameClassCallIndices): void
  675. {
  676. $this->testIsTheSameClassCall($code, $sameClassCallIndices);
  677. }
  678. /**
  679. * @return iterable<array{string, list<int>}>
  680. */
  681. public static function provideIsTheSameClassCall80Cases(): iterable
  682. {
  683. yield [
  684. '<?php
  685. class Foo {
  686. public function methodOne() {
  687. $x = $this?->otherMethod(1, 2, 3);
  688. }
  689. }
  690. ',
  691. [24],
  692. ];
  693. }
  694. /**
  695. * @param array<string, ArgumentAnalysis> $expected
  696. *
  697. * @dataProvider provideFunctionArgumentInfoPhp80Cases
  698. *
  699. * @requires PHP 8.0
  700. */
  701. public function testFunctionArgumentInfoPhp80(string $code, int $methodIndex, array $expected): void
  702. {
  703. $this->testFunctionArgumentInfo($code, $methodIndex, $expected);
  704. }
  705. public static function provideFunctionArgumentInfoPhp80Cases(): iterable
  706. {
  707. yield ['<?php function($aa,){};', 1, [
  708. '$aa' => new ArgumentAnalysis(
  709. '$aa',
  710. 3,
  711. null,
  712. null
  713. ),
  714. ]];
  715. yield ['<?php fn($a, $bc ,) => null;', 1, [
  716. '$a' => new ArgumentAnalysis(
  717. '$a',
  718. 3,
  719. null,
  720. null
  721. ),
  722. '$bc' => new ArgumentAnalysis(
  723. '$bc',
  724. 6,
  725. null,
  726. null
  727. ),
  728. ]];
  729. }
  730. /**
  731. * @param list<int> $expectedIndices
  732. */
  733. private static function assertIsGlobalFunctionCall(array $expectedIndices, string $code): void
  734. {
  735. $tokens = Tokens::fromCode($code);
  736. $analyzer = new FunctionsAnalyzer();
  737. $actualIndices = [];
  738. foreach ($tokens as $index => $token) {
  739. if ($analyzer->isGlobalFunctionCall($tokens, $index)) {
  740. $actualIndices[] = $index;
  741. }
  742. }
  743. self::assertSame(
  744. $expectedIndices,
  745. $actualIndices,
  746. \sprintf(
  747. 'Global function calls found at positions: [%s], expected at [%s].',
  748. implode(', ', $actualIndices),
  749. implode(', ', $expectedIndices)
  750. )
  751. );
  752. }
  753. }