Completion for keywords and bug fixes
parent
6adb3f48e1
commit
f6a7ce1a8b
|
@ -0,0 +1 @@
|
||||||
|
<
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
cl
|
|
@ -6,6 +6,8 @@ namespace LanguageServer;
|
||||||
use PhpParser\Node;
|
use PhpParser\Node;
|
||||||
use phpDocumentor\Reflection\Types;
|
use phpDocumentor\Reflection\Types;
|
||||||
use LanguageServer\Protocol\{
|
use LanguageServer\Protocol\{
|
||||||
|
TextEdit,
|
||||||
|
Range,
|
||||||
Position,
|
Position,
|
||||||
SymbolKind,
|
SymbolKind,
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
|
@ -14,6 +16,77 @@ use LanguageServer\Protocol\{
|
||||||
|
|
||||||
class CompletionProvider
|
class CompletionProvider
|
||||||
{
|
{
|
||||||
|
const KEYWORDS = [
|
||||||
|
'?>',
|
||||||
|
'__halt_compiler',
|
||||||
|
'abstract',
|
||||||
|
'and',
|
||||||
|
'array',
|
||||||
|
'as',
|
||||||
|
'break',
|
||||||
|
'callable',
|
||||||
|
'case',
|
||||||
|
'catch',
|
||||||
|
'class',
|
||||||
|
'clone',
|
||||||
|
'const',
|
||||||
|
'continue',
|
||||||
|
'declare',
|
||||||
|
'default',
|
||||||
|
'die',
|
||||||
|
'do',
|
||||||
|
'echo',
|
||||||
|
'else',
|
||||||
|
'elseif',
|
||||||
|
'empty',
|
||||||
|
'enddeclare',
|
||||||
|
'endfor',
|
||||||
|
'endforeach',
|
||||||
|
'endif',
|
||||||
|
'endswitch',
|
||||||
|
'endwhile',
|
||||||
|
'eval',
|
||||||
|
'exit',
|
||||||
|
'extends',
|
||||||
|
'final',
|
||||||
|
'finally',
|
||||||
|
'for',
|
||||||
|
'foreach',
|
||||||
|
'function',
|
||||||
|
'global',
|
||||||
|
'goto',
|
||||||
|
'if',
|
||||||
|
'implements',
|
||||||
|
'include',
|
||||||
|
'include_once',
|
||||||
|
'instanceof',
|
||||||
|
'insteadof',
|
||||||
|
'interface',
|
||||||
|
'isset',
|
||||||
|
'list',
|
||||||
|
'namespace',
|
||||||
|
'new',
|
||||||
|
'or',
|
||||||
|
'print',
|
||||||
|
'private',
|
||||||
|
'protected',
|
||||||
|
'public',
|
||||||
|
'require',
|
||||||
|
'require_once',
|
||||||
|
'return',
|
||||||
|
'static',
|
||||||
|
'switch',
|
||||||
|
'throw',
|
||||||
|
'trait',
|
||||||
|
'try',
|
||||||
|
'unset',
|
||||||
|
'use',
|
||||||
|
'var',
|
||||||
|
'while',
|
||||||
|
'xor',
|
||||||
|
'yield'
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DefinitionResolver
|
* @var DefinitionResolver
|
||||||
*/
|
*/
|
||||||
|
@ -38,12 +111,12 @@ class CompletionProvider
|
||||||
* Returns suggestions for a specific cursor position in a document
|
* Returns suggestions for a specific cursor position in a document
|
||||||
*
|
*
|
||||||
* @param PhpDocument $document The opened document
|
* @param PhpDocument $document The opened document
|
||||||
* @param Position $position The cursor position
|
* @param Position $pos The cursor position
|
||||||
* @return CompletionItem[]
|
* @return CompletionItem[]
|
||||||
*/
|
*/
|
||||||
public function provideCompletion(PhpDocument $document, Position $position): array
|
public function provideCompletion(PhpDocument $document, Position $pos): array
|
||||||
{
|
{
|
||||||
$node = $document->getNodeAtPosition($position);
|
$node = $document->getNodeAtPosition($pos);
|
||||||
|
|
||||||
if ($node instanceof Node\Expr\Error) {
|
if ($node instanceof Node\Expr\Error) {
|
||||||
$node = $node->getAttribute('parentNode');
|
$node = $node->getAttribute('parentNode');
|
||||||
|
@ -93,11 +166,12 @@ class CompletionProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
// A ConstFetch means any static reference, like a class, interface, etc.
|
// A ConstFetch means any static reference, like a class, interface, etc. or keyword
|
||||||
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|
||||||
|| $node instanceof Node\Expr\New_
|
|| $node instanceof Node\Expr\New_
|
||||||
) {
|
) {
|
||||||
$prefix = null;
|
$prefix = '';
|
||||||
|
$prefixLen = 0;
|
||||||
if ($node instanceof Node\Name) {
|
if ($node instanceof Node\Name) {
|
||||||
$isFullyQualified = $node->isFullyQualified();
|
$isFullyQualified = $node->isFullyQualified();
|
||||||
$prefix = (string)$node;
|
$prefix = (string)$node;
|
||||||
|
@ -115,7 +189,7 @@ class CompletionProvider
|
||||||
foreach ($stmt->uses as $use) {
|
foreach ($stmt->uses as $use) {
|
||||||
// Get the definition for the used namespace, class-like, function or constant
|
// Get the definition for the used namespace, class-like, function or constant
|
||||||
// And save it under the alias
|
// And save it under the alias
|
||||||
$fqn = (string)Node\Name::concat($stmt->prefix, $use->name);
|
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
|
||||||
$aliasedDefs[$use->alias] = $this->project->getDefinition($fqn);
|
$aliasedDefs[$use->alias] = $this->project->getDefinition($fqn);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -165,6 +239,16 @@ class CompletionProvider
|
||||||
$items[] = $item;
|
$items[] = $item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Suggest keywords
|
||||||
|
if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) {
|
||||||
|
foreach (self::KEYWORDS as $keyword) {
|
||||||
|
if (substr($keyword, 0, $prefixLen) === $prefix) {
|
||||||
|
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
|
||||||
|
$item->insertText = $keyword . ' ';
|
||||||
|
$items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
$node instanceof Node\Expr\Variable
|
$node instanceof Node\Expr\Variable
|
||||||
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
|
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
|
||||||
|
@ -180,6 +264,13 @@ class CompletionProvider
|
||||||
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
|
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
|
||||||
$items[] = $item;
|
$items[] = $item;
|
||||||
}
|
}
|
||||||
|
} else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
|
||||||
|
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
|
||||||
|
$item->textEdit = new TextEdit(
|
||||||
|
new Range($pos, $pos),
|
||||||
|
stripStringOverlap($document->getRange(new Range(new Position(0, 0), $pos)), '<?php')
|
||||||
|
);
|
||||||
|
$items[] = $item;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $items;
|
return $items;
|
||||||
|
|
|
@ -37,6 +37,13 @@ class Definition
|
||||||
*/
|
*/
|
||||||
public $isGlobal;
|
public $isGlobal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* False for instance methods and properties
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $isStatic;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the Definition is a class
|
* True if the Definition is a class
|
||||||
*
|
*
|
||||||
|
|
|
@ -107,6 +107,10 @@ class DefinitionResolver
|
||||||
|| $node instanceof Node\Stmt\Function_
|
|| $node instanceof Node\Stmt\Function_
|
||||||
|| $node->getAttribute('parentNode') instanceof Node\Stmt\Const_
|
|| $node->getAttribute('parentNode') instanceof Node\Stmt\Const_
|
||||||
);
|
);
|
||||||
|
$def->isStatic = (
|
||||||
|
($node instanceof Node\Stmt\ClassMethod && $node->isStatic())
|
||||||
|
|| ($node instanceof Node\Stmt\PropertyProperty && $node->getAttribute('parentNode')->isStatic())
|
||||||
|
);
|
||||||
$def->fqn = $fqn;
|
$def->fqn = $fqn;
|
||||||
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
|
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
|
||||||
$def->type = $this->getTypeFromNode($node);
|
$def->type = $this->getTypeFromNode($node);
|
||||||
|
@ -664,28 +668,35 @@ class DefinitionResolver
|
||||||
if ($node instanceof Node\Param) {
|
if ($node instanceof Node\Param) {
|
||||||
// Parameters
|
// Parameters
|
||||||
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
||||||
if (
|
if ($docBlock !== null) {
|
||||||
$docBlock !== null
|
|
||||||
&& !empty($paramTags = $docBlock->getTagsByName('param'))
|
|
||||||
&& $paramTags[0]->getType() !== null
|
|
||||||
) {
|
|
||||||
// Use @param tag
|
// Use @param tag
|
||||||
return $paramTags[0]->getType();
|
foreach ($docBlock->getTagsByName('param') as $paramTag) {
|
||||||
|
if ($paramTag->getVariableName() === $node->name) {
|
||||||
|
if ($paramTag->getType() === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $paramTag->getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($node->type !== null) {
|
if ($node->type !== null) {
|
||||||
// Use PHP7 return type hint
|
// Use PHP7 return type hint
|
||||||
if (is_string($node->type)) {
|
if (is_string($node->type)) {
|
||||||
// Resolve a string like "bool" to a type object
|
// Resolve a string like "bool" to a type object
|
||||||
$type = $this->typeResolver->resolve($node->type);
|
$type = $this->typeResolver->resolve($node->type);
|
||||||
}
|
} else {
|
||||||
$type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
|
$type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
|
||||||
if ($node->default !== null) {
|
|
||||||
$defaultType = $this->resolveExpressionNodeToType($node->default);
|
|
||||||
$type = new Types\Compound([$type, $defaultType]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unknown parameter type
|
if ($node->default !== null) {
|
||||||
return new Types\Mixed;
|
$defaultType = $this->resolveExpressionNodeToType($node->default);
|
||||||
|
if (isset($type) && !is_a($type, get_class($defaultType))) {
|
||||||
|
$type = new Types\Compound([$type, $defaultType]);
|
||||||
|
} else {
|
||||||
|
$type = $defaultType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $type ?? new Types\Mixed;
|
||||||
}
|
}
|
||||||
if ($node instanceof Node\FunctionLike) {
|
if ($node instanceof Node\FunctionLike) {
|
||||||
// Functions/methods
|
// Functions/methods
|
||||||
|
|
|
@ -300,6 +300,22 @@ class PhpDocument
|
||||||
return $finder->node;
|
return $finder->node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a range of the content
|
||||||
|
*
|
||||||
|
* @param Range $range
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getRange(Range $range)
|
||||||
|
{
|
||||||
|
if ($this->content === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$start = $range->start->toOffset($this->content);
|
||||||
|
$length = $range->end->toOffset($this->content) - $start;
|
||||||
|
return substr($this->content, $start, $length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the definition node for a fully qualified name
|
* Returns the definition node for a fully qualified name
|
||||||
*
|
*
|
||||||
|
|
|
@ -156,6 +156,9 @@ class CompletionItem
|
||||||
if ($def->documentation) {
|
if ($def->documentation) {
|
||||||
$item->documentation = $def->documentation;
|
$item->documentation = $def->documentation;
|
||||||
}
|
}
|
||||||
|
if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
|
||||||
|
$item->insertText = '$' . $def->symbolInformation->name;
|
||||||
|
}
|
||||||
return $item;
|
return $item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,4 +49,16 @@ class Position
|
||||||
|
|
||||||
return $this->character - $position->character;
|
return $this->character - $position->character;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the offset of the position in a string
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function toOffset(string $content): int
|
||||||
|
{
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
return array_sum(array_map('strlen', array_slice($lines, 0, $this->line))) + $this->character;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,3 +95,25 @@ function getClosestNode(Node $node, string $type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the part of $b that is not overlapped by $a
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* stripStringOverlap('whatever<?', '<?php') === 'php'
|
||||||
|
*
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function stripStringOverlap(string $a, string $b): string
|
||||||
|
{
|
||||||
|
$aLen = strlen($a);
|
||||||
|
$bLen = strlen($b);
|
||||||
|
for ($i = 1; $i <= $bLen; $i++) {
|
||||||
|
if (substr($b, 0, $i) === substr($a, $aLen - $i)) {
|
||||||
|
return substr($b, $i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $b;
|
||||||
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ class LanguageServerTest extends TestCase
|
||||||
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
||||||
if ($msg->body->params->type === MessageType::ERROR) {
|
if ($msg->body->params->type === MessageType::ERROR) {
|
||||||
$promise->reject(new Exception($msg->body->params->message));
|
$promise->reject(new Exception($msg->body->params->message));
|
||||||
} else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) {
|
} else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) {
|
||||||
$promise->fulfill();
|
$promise->fulfill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ class LanguageServerTest extends TestCase
|
||||||
if ($promise->state === Promise::PENDING) {
|
if ($promise->state === Promise::PENDING) {
|
||||||
$promise->reject(new Exception($msg->body->params->message));
|
$promise->reject(new Exception($msg->body->params->message));
|
||||||
}
|
}
|
||||||
} else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) {
|
} else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) {
|
||||||
// Indexing finished
|
// Indexing finished
|
||||||
$promise->fulfill();
|
$promise->fulfill();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,16 @@ namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use LanguageServer\Tests\MockProtocolStream;
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
use LanguageServer\{Server, LanguageClient, Project};
|
use LanguageServer\{Server, LanguageClient, Project, CompletionProvider};
|
||||||
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ClientCapabilities, CompletionItem, CompletionItemKind};
|
use LanguageServer\Protocol\{
|
||||||
|
TextDocumentIdentifier,
|
||||||
|
TextEdit,
|
||||||
|
Range,
|
||||||
|
Position,
|
||||||
|
ClientCapabilities,
|
||||||
|
CompletionItem,
|
||||||
|
CompletionItemKind
|
||||||
|
};
|
||||||
use function LanguageServer\pathToUri;
|
use function LanguageServer\pathToUri;
|
||||||
|
|
||||||
class CompletionTest extends TestCase
|
class CompletionTest extends TestCase
|
||||||
|
@ -168,7 +176,10 @@ class CompletionTest extends TestCase
|
||||||
'staticTestProperty',
|
'staticTestProperty',
|
||||||
CompletionItemKind::PROPERTY,
|
CompletionItemKind::PROPERTY,
|
||||||
'\TestClass[]',
|
'\TestClass[]',
|
||||||
'Lorem excepteur officia sit anim velit veniam enim.'
|
'Lorem excepteur officia sit anim velit veniam enim.',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'$staticTestProperty'
|
||||||
)
|
)
|
||||||
], $items);
|
], $items);
|
||||||
}
|
}
|
||||||
|
@ -192,7 +203,10 @@ class CompletionTest extends TestCase
|
||||||
'staticTestProperty',
|
'staticTestProperty',
|
||||||
CompletionItemKind::PROPERTY,
|
CompletionItemKind::PROPERTY,
|
||||||
'\TestClass[]',
|
'\TestClass[]',
|
||||||
'Lorem excepteur officia sit anim velit veniam enim.'
|
'Lorem excepteur officia sit anim velit veniam enim.',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'$staticTestProperty'
|
||||||
),
|
),
|
||||||
new CompletionItem(
|
new CompletionItem(
|
||||||
'staticTestMethod',
|
'staticTestMethod',
|
||||||
|
@ -259,4 +273,62 @@ class CompletionTest extends TestCase
|
||||||
)
|
)
|
||||||
], $items);
|
], $items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testKeywords()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(2, 1)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals([
|
||||||
|
new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '),
|
||||||
|
new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ')
|
||||||
|
], $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHtmlWithoutPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(0, 0)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals([
|
||||||
|
new CompletionItem(
|
||||||
|
'<?php',
|
||||||
|
CompletionItemKind::KEYWORD,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), '<?php')
|
||||||
|
)
|
||||||
|
], $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHtmlWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(0, 1)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals([
|
||||||
|
new CompletionItem(
|
||||||
|
'<?php',
|
||||||
|
CompletionItemKind::KEYWORD,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(0, 1), new Position(0, 1)), '?php')
|
||||||
|
)
|
||||||
|
], $items);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Utils;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use function LanguageServer\stripStringOverlap;
|
||||||
|
|
||||||
|
class StripStringOverlapTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testNoCharOverlaps()
|
||||||
|
{
|
||||||
|
$this->assertEquals('<?php', stripStringOverlap('bla', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test1CharOverlaps()
|
||||||
|
{
|
||||||
|
$this->assertEquals('?php', stripStringOverlap('bla<', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test2CharsOverlap()
|
||||||
|
{
|
||||||
|
$this->assertEquals('php', stripStringOverlap('bla<?', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEverythingOverlaps()
|
||||||
|
{
|
||||||
|
$this->assertEquals('', stripStringOverlap('bla<?php', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyA()
|
||||||
|
{
|
||||||
|
$this->assertEquals('<?php', stripStringOverlap('', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyB()
|
||||||
|
{
|
||||||
|
$this->assertEquals('', stripStringOverlap('bla', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBothEmpty()
|
||||||
|
{
|
||||||
|
$this->assertEquals('', stripStringOverlap('', ''));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue