* Dariusz Rumiński * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Tests\Tokenizer; use PhpCsFixer\Tests\Test\Assert\AssertTokensTrait; use PhpCsFixer\Tests\TestCase; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; /** * @author Dariusz Rumiński * * @internal * * @covers \PhpCsFixer\Tokenizer\Tokens */ final class TokensTest extends TestCase { use AssertTokensTrait; public function testReadFromCacheAfterClearing(): void { $code = 'count(); for ($i = 0; $i < $countBefore; ++$i) { $tokens->clearAt($i); } $tokens = Tokens::fromCode($code); static::assertSame($countBefore, $tokens->count()); } /** * @param Token[] $sequence * @param null|array|bool $caseSensitive * * @dataProvider provideFindSequenceCases */ public function testFindSequence( string $source, ?array $expected, array $sequence, int $start = 0, int $end = null, $caseSensitive = true ): void { $tokens = Tokens::fromCode($source); static::assertEqualsTokensArray( $expected, $tokens->findSequence( $sequence, $start, $end, $caseSensitive ) ); } public function provideFindSequenceCases(): array { return [ [ ' new Token([T_OPEN_TAG, ' new Token([T_VARIABLE, '$x']), ], [ [T_OPEN_TAG], [T_VARIABLE, '$x'], ], ], [ ' new Token('='), 5 => new Token([T_LNUMBER, '4']), 6 => new Token(';'), ], [ '=', [T_LNUMBER, '4'], ';', ], ], [ ' new Token([T_OPEN_TAG, ' new Token([T_VARIABLE, '$x']), ], [ [T_OPEN_TAG], [T_VARIABLE, '$x'], ], 0, ], [ ' new Token('='), 5 => new Token([T_LNUMBER, '7']), 6 => new Token(';'), ], [ '=', [T_LNUMBER, '7'], ';', ], 3, 6, ], [ ' new Token([T_OPEN_TAG, ' new Token([T_VARIABLE, '$x']), ], [ [T_OPEN_TAG], [T_VARIABLE, '$x'], ], 0, 1, true, ], [ ' true], ], [ ' new Token([T_OPEN_TAG, ' new Token([T_VARIABLE, '$x']), ], [ [T_OPEN_TAG], [T_VARIABLE, '$X'], ], 0, 1, false, ], [ ' new Token([T_OPEN_TAG, ' new Token([T_VARIABLE, '$x']), ], [ [T_OPEN_TAG], [T_VARIABLE, '$X'], ], 0, 1, [1 => false], ], [ ' new Token([T_OPEN_TAG, ' new Token([T_VARIABLE, '$x']), ], [ [T_OPEN_TAG], [T_VARIABLE, '$X'], ], 0, 1, [1 => false], ], [ ' false], ], [ 'expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($message); $tokens = Tokens::fromCode('findSequence($sequence); } public function provideFindSequenceExceptionCases(): array { $emptyToken = new Token(''); return [ ['Invalid sequence.', []], [ 'Non-meaningful token at position: "0".', [[T_WHITESPACE, ' ']], ], [ 'Non-meaningful token at position: "1".', ['{', [T_COMMENT, '// Foo'], '}'], ], [ 'Non-meaningful (empty) token at position: "2".', ['{', '!', $emptyToken, '}'], ], ]; } public function testClearRange(): void { $source = <<<'PHP' findGivenKind(T_PUBLIC)); $tokens->clearRange($fooIndex, $barIndex - 1); $newPublicIndexes = array_keys($tokens->findGivenKind(T_PUBLIC)); static::assertSame($barIndex, reset($newPublicIndexes)); for ($i = $fooIndex; $i < $barIndex; ++$i) { static::assertTrue($tokens[$i]->isWhitespace()); } } /** * @dataProvider provideMonolithicPhpDetectionCases */ public function testMonolithicPhpDetection(string $source, bool $isMonolithic): void { $tokens = Tokens::fromCode($source); static::assertSame($isMonolithic, $tokens->isMonolithicPhp()); } public function provideMonolithicPhpDetectionCases(): array { return [ ["", true], ['', false], [' ', false], ["#!/usr/bin/env php\n ", false], ["isMonolithicPhp()); } public function provideShortOpenTagMonolithicPhpDetectionCases(): array { return [ ["", true], [" ", false], ["isMonolithicPhp()); } public function provideShortOpenTagEchoMonolithicPhpDetectionCases(): array { return [ ["", true], [" ", false], ["isTokenKindFound(T_CLASS)); static::assertTrue($tokens->isTokenKindFound(T_RETURN)); static::assertFalse($tokens->isTokenKindFound(T_INTERFACE)); static::assertFalse($tokens->isTokenKindFound(T_ARRAY)); static::assertTrue($tokens->isAllTokenKindsFound([T_CLASS, T_RETURN])); static::assertFalse($tokens->isAllTokenKindsFound([T_CLASS, T_INTERFACE])); static::assertTrue($tokens->isAnyTokenKindsFound([T_CLASS, T_RETURN])); static::assertTrue($tokens->isAnyTokenKindsFound([T_CLASS, T_INTERFACE])); static::assertFalse($tokens->isAnyTokenKindsFound([T_INTERFACE, T_ARRAY])); } public function testFindGivenKind(): void { $source = <<<'PHP' findGivenKind(T_CLASS); static::assertCount(1, $found); static::assertArrayHasKey(1, $found); static::assertSame(T_CLASS, $found[1]->getId()); $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION]); static::assertCount(2, $found); static::assertArrayHasKey(T_CLASS, $found); static::assertIsArray($found[T_CLASS]); static::assertCount(1, $found[T_CLASS]); static::assertArrayHasKey(1, $found[T_CLASS]); static::assertSame(T_CLASS, $found[T_CLASS][1]->getId()); static::assertArrayHasKey(T_FUNCTION, $found); static::assertIsArray($found[T_FUNCTION]); static::assertCount(2, $found[T_FUNCTION]); static::assertArrayHasKey(9, $found[T_FUNCTION]); static::assertSame(T_FUNCTION, $found[T_FUNCTION][9]->getId()); static::assertArrayHasKey(26, $found[T_FUNCTION]); static::assertSame(T_FUNCTION, $found[T_FUNCTION][26]->getId()); // test offset and limits of the search $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION], 10); static::assertCount(0, $found[T_CLASS]); static::assertCount(1, $found[T_FUNCTION]); static::assertArrayHasKey(26, $found[T_FUNCTION]); $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION], 2, 10); static::assertCount(0, $found[T_CLASS]); static::assertCount(1, $found[T_FUNCTION]); static::assertArrayHasKey(9, $found[T_FUNCTION]); } /** * @param Token[] $expected tokens * @param int[] $indexes to clear * * @dataProvider provideGetClearTokenAndMergeSurroundingWhitespaceCases */ public function testClearTokenAndMergeSurroundingWhitespace(string $source, array $indexes, array $expected): void { $this->doTestClearTokens($source, $indexes, $expected); if (\count($indexes) > 1) { $this->doTestClearTokens($source, array_reverse($indexes), $expected); } } public function provideGetClearTokenAndMergeSurroundingWhitespaceCases(): array { $clearToken = new Token(''); return [ [ 'getNextTokenOfKind($index, $findTokens, $caseSensitive)); } else { static::assertSame($expectedIndex, $tokens->getPrevTokenOfKind($index, $findTokens, $caseSensitive)); } static::assertSame($expectedIndex, $tokens->getTokenOfKindSibling($index, $direction, $findTokens, $caseSensitive)); } public function provideTokenOfKindSiblingCases(): array { return [ // find next cases [ 35, 1, 34, [';'], ], [ 14, 1, 0, [[T_RETURN]], ], [ 32, 1, 14, [[T_RETURN]], ], [ 6, 1, 0, [[T_RETURN], [T_FUNCTION]], ], // find previous cases [ 14, -1, 32, [[T_RETURN], [T_FUNCTION]], ], [ 6, -1, 7, [[T_FUNCTION]], ], [ null, -1, 6, [[T_FUNCTION]], ], ]; } /** * @dataProvider provideFindBlockEndCases */ public function testFindBlockEnd(int $expectedIndex, string $source, int $type, int $searchIndex): void { static::assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex); } public function provideFindBlockEndCases(): array { return [ [4, '{$bar};', Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, 3], [4, '', Tokens::BLOCK_TYPE_CURLY_BRACE, 5], [11, '{"set_{$name}"}(42);', Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, 3], [19, 'expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/^Invalid param type: "-1"\.$/'); Tokens::clearCache(); $tokens = Tokens::fromCode('findBlockEnd(-1, 0); } public function testFindBlockEndInvalidStart(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/^Invalid param \$startIndex - not a proper block "start"\.$/'); Tokens::clearCache(); $tokens = Tokens::fromCode('findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, 0); } public function testFindBlockEndCalledMultipleTimes(): void { Tokens::clearCache(); $tokens = Tokens::fromCode('findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2)); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/^Invalid param \$startIndex - not a proper block "start"\.$/'); $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 7); } public function testFindBlockStartEdgeCalledMultipleTimes(): void { Tokens::clearCache(); $tokens = Tokens::fromCode('findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 7)); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/^Invalid param \$startIndex - not a proper block "end"\.$/'); $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2); } public function testEmptyTokens(): void { $code = ''; $tokens = Tokens::fromCode($code); static::assertCount(0, $tokens); static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG)); } public function testEmptyTokensMultiple(): void { $code = ''; $tokens = Tokens::fromCode($code); static::assertFalse($tokens->isChanged()); $tokens->insertAt(0, new Token([T_WHITESPACE, ' '])); static::assertCount(1, $tokens); static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG)); static::assertTrue($tokens->isChanged()); $tokens2 = Tokens::fromCode($code); static::assertCount(0, $tokens2); static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG)); } public function testFromArray(): void { $code = 'toArray()); static::assertTrue($tokens1->isTokenKindFound(T_OPEN_TAG)); static::assertTrue($tokens2->isTokenKindFound(T_OPEN_TAG)); static::assertSame($tokens1->getCodeHash(), $tokens2->getCodeHash()); } public function testFromArrayEmpty(): void { $tokens = Tokens::fromArray([]); static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG)); } /** * @dataProvider provideIsEmptyCases */ public function testIsEmpty(Token $token, bool $isEmpty): void { $tokens = Tokens::fromArray([$token]); Tokens::clearCache(); static::assertSame($isEmpty, $tokens->isEmptyAt(0), $token->toJson()); } public function provideIsEmptyCases(): array { return [ [new Token(''), true], [new Token('('), false], [new Token([T_WHITESPACE, ' ']), false], ]; } public function testClone(): void { $code = 'isTokenKindFound(T_OPEN_TAG)); static::assertTrue($tokensClone->isTokenKindFound(T_OPEN_TAG)); $count = \count($tokens); static::assertCount($count, $tokensClone); for ($i = 0; $i < $count; ++$i) { static::assertTrue($tokens[$i]->equals($tokensClone[$i])); static::assertNotSame($tokens[$i], $tokensClone[$i]); } } /** * @dataProvider provideEnsureWhitespaceAtIndexCases */ public function testEnsureWhitespaceAtIndex(string $expected, string $input, int $index, int $offset, string $whiteSpace): void { $tokens = Tokens::fromCode($input); $tokens->ensureWhitespaceAtIndex($index, $offset, $whiteSpace); $tokens->clearEmptyTokens(); static::assertTokens(Tokens::fromCode($expected), $tokens); } public function provideEnsureWhitespaceAtIndexCases(): array { return [ [ 'name = $name; } }'; $tokens = Tokens::fromCode(sprintf($template, '')); $commentIndex = $tokens->getNextTokenOfKind(0, [[T_COMMENT]]); $tokens->insertAt( $commentIndex, [ new Token([T_PRIVATE, 'private']), new Token([T_WHITESPACE, ' ']), new Token([T_VARIABLE, '$name']), new Token(';'), ] ); static::assertTrue($tokens->isChanged()); $expected = Tokens::fromCode(sprintf($template, 'private $name;')); static::assertFalse($expected->isChanged()); static::assertTokens($expected, $tokens); } /** * @dataProvider provideRemoveLeadingWhitespaceCases */ public function testRemoveLeadingWhitespace(int $index, ?string $whitespaces, string $expected, string $input = null): void { Tokens::clearCache(); $tokens = Tokens::fromCode($input ?? $expected); $tokens->removeLeadingWhitespace($index, $whitespaces); static::assertSame($expected, $tokens->generateCode()); } public function provideRemoveLeadingWhitespaceCases(): array { return [ [ 7, null, "removeTrailingWhitespace($index, $whitespaces); static::assertSame($expected, $tokens->generateCode()); } public function provideRemoveTrailingWhitespaceCases(): \Generator { $leadingCases = $this->provideRemoveLeadingWhitespaceCases(); foreach ($leadingCases as $leadingCase) { $leadingCase[0] -= 2; yield $leadingCase; } } public function testRemovingLeadingWhitespaceWithEmptyTokenInCollection(): void { $code = "clearAt(2); $tokens->removeLeadingWhitespace(3); $tokens->clearEmptyTokens(); static::assertTokens(Tokens::fromCode("clearAt(2); $tokens->removeTrailingWhitespace(1); $tokens->clearEmptyTokens(); static::assertTokens(Tokens::fromCode("count(); $tokens->removeLeadingWhitespace(4); static::assertSame($originalCount, $tokens->count()); static::assertSame( 'generateCode() ); } /** * Action that begins with the word "remove" should not change the size of collection. */ public function testRemovingTrailingWhitespaceWillNotIncreaseTokensCount(): void { $tokens = Tokens::fromCode('count(); $tokens->removeTrailingWhitespace(2); static::assertSame($originalCount, $tokens->count()); static::assertSame( 'generateCode() ); } /** * @dataProvider provideDetectBlockTypeCases */ public function testDetectBlockType(?array $expected, string $code, int $index): void { $tokens = Tokens::fromCode($code); static::assertSame($expected, Tokens::detectBlockType($tokens[$index])); } public function provideDetectBlockTypeCases(): \Generator { yield [ [ 'type' => Tokens::BLOCK_TYPE_CURLY_BRACE, 'isStart' => true, ], 'overrideRange($indexStart, $indexEnd, $items); $tokens->clearEmptyTokens(); self::assertTokens(Tokens::fromArray($expected), $tokens); } /** * @param int $indexStart start overriding index * @param int $indexEnd end overriding index * @param array $items tokens to insert * * @dataProvider provideOverrideRangeCases */ public function testOverrideRange(array $expected, string $code, int $indexStart, int $indexEnd, array $items): void { $tokens = Tokens::fromCode($code); $tokens->overrideRange($indexStart, $indexEnd, $items); $tokens->clearEmptyTokens(); self::assertTokens(Tokens::fromArray($expected), $tokens); } public function provideOverrideRangeCases(): \Generator { // typically done by transformers, here we test the reverse yield 'override different tokens but same content' => [ [ new Token([T_OPEN_TAG, ' [ [ new Token([T_OPEN_TAG, "isChanged()); $tokens = Tokens::fromArray( [ new Token([T_OPEN_TAG, "isChanged()); } /** * @dataProvider provideGetMeaningfulTokenSiblingCases */ public function testGetMeaningfulTokenSibling(?int $expectIndex, int $index, int $direction, string $source): void { Tokens::clearCache(); $tokens = Tokens::fromCode($source); static::assertSame($expectIndex, $tokens->getMeaningfulTokenSibling($index, $direction)); } public function provideGetMeaningfulTokenSiblingCases(): \Generator { yield [null, 0, 1, ''.$i => [3, $i, 1, '>' => [4, 3, 1, ' [null, 6, 1, ' [null, 888, 1, ' $slices */ public function testInsertSlicesAtMultiplePlaces(string $expected, array $slices): void { $input = <<<'EOF' insertSlices([ 16 => $slices, 6 => $slices, ]); static::assertTokens(Tokens::fromCode($expected), $tokens); } public function provideInsertSlicesAtMultiplePlacesCases(): \Generator { yield 'one slice count' => [ <<<'EOF' [ <<<'EOF' [ <<<'EOF' isChanged()); static::assertFalse($tokens->isTokenKindFound(T_COMMENT)); static::assertSame(5, $tokens->getSize()); $tokens->insertSlices([1 => new Token([T_COMMENT, '/* comment */'])]); static::assertTrue($tokens->isChanged()); static::assertTrue($tokens->isTokenKindFound(T_COMMENT)); static::assertSame(6, $tokens->getSize()); } /** * @dataProvider provideInsertSlicesCases */ public function testInsertSlices(Tokens $expected, Tokens $tokens, array $slices): void { $tokens->insertSlices($slices); static::assertTokens($expected, $tokens); } public function provideInsertSlicesCases(): iterable { // basic insert of single token at 3 different locations including appending as new token $template = " [ Tokens::fromCode(sprintf($template, $commentContent, '', '')), clone $from, [1 => $commentToken], ]; yield 'single insert @ 3' => [ Tokens::fromCode(sprintf($template, '', $commentContent, '')), clone $from, [3 => Tokens::fromArray([$commentToken])], ]; yield 'single insert @ 9' => [ Tokens::fromCode(sprintf($template, '', '', $commentContent)), clone $from, [9 => [$commentToken]], ]; // basic tests for single token, array of that token and tokens object with that token $openTagToken = new Token([T_OPEN_TAG, " $openTagToken], [0 => [clone $openTagToken]], [0 => clone Tokens::fromArray([$openTagToken])], ]; foreach ($slices as $i => $slice) { yield 'insert open tag @ 0 into empty collection '.$i => [$expected, new Tokens(), $slice]; } // test insert lists of tokens, index out of order $setOne = [ new Token([T_ECHO, 'echo']), new Token([T_WHITESPACE, ' ']), new Token([T_CONSTANT_ENCAPSED_STRING, '"new"']), new Token(';'), ]; $setTwo = [ new Token([T_WHITESPACE, ' ']), new Token([T_COMMENT, '/* new comment */']), ]; $setThree = Tokens::fromArray([ new Token([T_VARIABLE, '$new']), new Token([T_WHITESPACE, ' ']), new Token('='), new Token([T_WHITESPACE, ' ']), new Token([T_LNUMBER, '8899']), new Token(';'), new Token([T_WHITESPACE, "\n"]), ]); $template = " [$expected, $from, [9 => $setThree, 1 => $setOne, 3 => $setTwo]]; $sets = []; for ($j = 0; $j < 4; ++$j) { $set = ['tokens' => [], 'content' => '']; for ($i = 0; $i < 10; ++$i) { $content = sprintf('/* new %d|%s */', $j, $i); $set['tokens'][] = new Token([T_COMMENT, $content]); $set['content'] .= $content; } $sets[$j] = $set; } yield 'overlapping inserts of bunch of comments ' => [ Tokens::fromCode(sprintf(" $sets[0]['tokens'], 3 => $sets[1]['tokens'], 5 => $sets[2]['tokens'], 6 => $sets[3]['tokens']], ]; } private static function assertFindBlockEnd(int $expectedIndex, string $source, int $type, int $searchIndex): void { Tokens::clearCache(); $tokens = Tokens::fromCode($source); static::assertSame($expectedIndex, $tokens->findBlockEnd($type, $searchIndex)); static::assertSame($searchIndex, $tokens->findBlockStart($type, $expectedIndex)); $detectedType = Tokens::detectBlockType($tokens[$searchIndex]); static::assertIsArray($detectedType); static::assertArrayHasKey('type', $detectedType); static::assertArrayHasKey('isStart', $detectedType); static::assertSame($type, $detectedType['type']); static::assertTrue($detectedType['isStart']); $detectedType = Tokens::detectBlockType($tokens[$expectedIndex]); static::assertIsArray($detectedType); static::assertArrayHasKey('type', $detectedType); static::assertArrayHasKey('isStart', $detectedType); static::assertSame($type, $detectedType['type']); static::assertFalse($detectedType['isStart']); } /** * @param null|Token[] $expected * @param null|Token[] $input */ private static function assertEqualsTokensArray(array $expected = null, array $input = null): void { if (null === $expected) { static::assertNull($input); return; } if (null === $input) { static::fail('While "input" is , "expected" is not.'); } static::assertSame(array_keys($expected), array_keys($input), 'Both arrays need to have same keys.'); foreach ($expected as $index => $expectedToken) { static::assertTrue( $expectedToken->equals($input[$index]), sprintf('The token at index %d should be %s, got %s', $index, $expectedToken->toJson(), $input[$index]->toJson()) ); } } /** * @param int[] $indexes * @param Token[] $expected */ private function doTestClearTokens(string $source, array $indexes, array $expected): void { Tokens::clearCache(); $tokens = Tokens::fromCode($source); foreach ($indexes as $index) { $tokens->clearTokenAndMergeSurroundingWhitespace($index); } static::assertSame(\count($expected), $tokens->count()); foreach ($expected as $index => $expectedToken) { $token = $tokens[$index]; $expectedPrototype = $expectedToken->getPrototype(); if (\is_array($expectedPrototype)) { unset($expectedPrototype[2]); // don't compare token lines as our token mutations don't deal with line numbers } static::assertTrue($token->equals($expectedPrototype), sprintf('The token at index %d should be %s, got %s', $index, json_encode($expectedPrototype), $token->toJson())); } } }