Initial import

This commit is contained in:
Nextcloud Team 2021-11-30 11:20:42 +01:00 committed by Lukas Reschke
commit 2295a33590
884 changed files with 93939 additions and 0 deletions

View file

@ -0,0 +1,7 @@
# .gitattributes
tests/ export-ignore
phpunit.xml.dist export-ignore
.travis.yml export-ignore
# Auto detect text files and perform LF normalization
* text=auto

View file

@ -0,0 +1,65 @@
name: CI
on:
push:
branches: ['master']
pull_request:
branches: ['*']
schedule:
- cron: '0 0 * * *'
jobs:
tests:
name: Tests - PHP ${{ matrix.php }} ${{ matrix.dependency-version }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
php: [ '7.2', '7.3', '7.4', '8.0' ]
dependency-version: [ '' ]
include:
- php: '7.2'
dependency-version: '--prefer-lowest'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
- name: Cache Composer dependencies
uses: actions/cache@v2
with:
path: ~/.composer/cache
key: php-${{ matrix.php }}-composer-locked-${{ hashFiles('composer.lock') }}
restore-keys: php-${{ matrix.php }}-composer-locked-
- name: Install PHP dependencies
run: composer update ${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-progress --no-suggest
- name: PHPUnit
run: vendor/bin/phpunit
cs:
name: Coding standards
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
tools: composer:v2, cs2pr
coverage: none
- name: Cache Composer dependencies
uses: actions/cache@v2
with:
path: ~/.composer/cache
key: php-74-composer-locked-${{ hashFiles('composer.lock') }}
restore-keys: php-74-composer-locked-
- name: Install PHP dependencies
run: composer install --no-interaction --no-progress --no-suggest
- name: PHP CodeSniffer
run: vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr

View file

@ -0,0 +1,7 @@
.DS_Store
.idea/*
vendor/*
composer.phar
composer.lock
.phpcs-cache
.phpunit.result.cache

View file

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="."/>
<arg name="extensions" value="php"/>
<arg name="cache" value=".phpcs-cache"/>
<!-- Show sniff names -->
<arg value="s"/>
<file>src</file>
<exclude-pattern>src/PhpDocReader/PhpParser/TokenParser.php</exclude-pattern>
<file>tests</file>
<exclude-pattern>tests/Fixtures</exclude-pattern>
<rule ref="HardMode"/>
<!-- Ignore PhpDocReader\AnnotationException -->
<rule ref="SlevomatCodingStandard.Classes.SuperfluousExceptionNaming.SuperfluousSuffix">
<severity>0</severity>
</rule>
</ruleset>

16
php/vendor/php-di/phpdoc-reader/LICENSE vendored Normal file
View file

@ -0,0 +1,16 @@
Copyright (C) 2019 Matthieu Napoli
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,57 @@
# PhpDocReader
![](https://img.shields.io/packagist/dt/PHP-DI/phpdoc-reader.svg)
This project is used by:
- [PHP-DI 6](http://php-di.org/)
- [phockito-unit-php-di](https://github.com/balihoo/phockito-unit-php-di)
Fork the README to add your project here.
## Features
PhpDocReader parses `@var` and `@param` values in PHP docblocks:
```php
use My\Cache\Backend;
class Cache
{
/**
* @var Backend
*/
protected $backend;
/**
* @param Backend $backend
*/
public function __construct($backend)
{
}
}
```
It supports namespaced class names with the same resolution rules as PHP:
- fully qualified name (starting with `\`)
- imported class name (eg. `use My\Cache\Backend;`)
- relative class name (from the current namespace, like `SubNamespace\MyClass`)
- aliased class name (eg. `use My\Cache\Backend as FooBar;`)
Primitive types (`@var string`) are ignored (returns null), only valid class names are returned.
## Usage
```php
$reader = new PhpDocReader();
// Read a property type (@var phpdoc)
$property = new ReflectionProperty($className, $propertyName);
$propertyClass = $reader->getPropertyClass($property);
// Read a parameter type (@param phpdoc)
$parameter = new ReflectionParameter(array($className, $methodName), $parameterName);
$parameterClass = $reader->getParameterClass($parameter);
```

View file

@ -0,0 +1,24 @@
{
"name": "php-di/phpdoc-reader",
"type": "library",
"description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)",
"keywords": ["phpdoc", "reflection"],
"license": "MIT",
"autoload": {
"psr-4": {
"PhpDocReader\\": "src/PhpDocReader"
}
},
"autoload-dev": {
"psr-4": {
"UnitTest\\PhpDocReader\\": "tests/"
}
},
"require": {
"php": ">=7.2.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5|^9.0",
"mnapoli/hard-mode": "~0.3.0"
}
}

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace PhpDocReader;
/**
* We stumbled upon an invalid class/property/method annotation.
*/
class AnnotationException extends \Exception
{
}

View file

@ -0,0 +1,307 @@
<?php declare(strict_types=1);
namespace PhpDocReader;
use PhpDocReader\PhpParser\UseStatementParser;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use Reflector;
/**
* PhpDoc reader
*/
class PhpDocReader
{
/** @var UseStatementParser */
private $parser;
private const PRIMITIVE_TYPES = [
'bool' => 'bool',
'boolean' => 'bool',
'string' => 'string',
'int' => 'int',
'integer' => 'int',
'float' => 'float',
'double' => 'float',
'array' => 'array',
'object' => 'object',
'callable' => 'callable',
'resource' => 'resource',
'mixed' => 'mixed',
'iterable' => 'iterable',
];
/** @var bool */
private $ignorePhpDocErrors;
/**
* @param bool $ignorePhpDocErrors Enable or disable throwing errors when PhpDoc errors occur (when parsing annotations).
*/
public function __construct(bool $ignorePhpDocErrors = false)
{
$this->parser = new UseStatementParser;
$this->ignorePhpDocErrors = $ignorePhpDocErrors;
}
/**
* Parse the docblock of the property to get the type (class or primitive type) of the var annotation.
*
* @return string|null Type of the property (content of var annotation)
* @throws AnnotationException
*/
public function getPropertyType(ReflectionProperty $property): ?string
{
return $this->readPropertyType($property, true);
}
/**
* Parse the docblock of the property to get the class of the var annotation.
*
* @return string|null Type of the property (content of var annotation)
* @throws AnnotationException
*/
public function getPropertyClass(ReflectionProperty $property): ?string
{
return $this->readPropertyType($property, false);
}
private function readPropertyType(ReflectionProperty $property, bool $allowPrimitiveTypes): ?string
{
// Get the content of the @var annotation
$docComment = $property->getDocComment();
if (! $docComment) {
return null;
}
if (preg_match('/@var\s+([^\s]+)/', $docComment, $matches)) {
[, $type] = $matches;
} else {
return null;
}
// Ignore primitive types
if (isset(self::PRIMITIVE_TYPES[$type])) {
if ($allowPrimitiveTypes) {
return self::PRIMITIVE_TYPES[$type];
}
return null;
}
// Ignore types containing special characters ([], <> ...)
if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
return null;
}
$class = $property->getDeclaringClass();
// If the class name is not fully qualified (i.e. doesn't start with a \)
if ($type[0] !== '\\') {
// Try to resolve the FQN using the class context
$resolvedType = $this->tryResolveFqn($type, $class, $property);
if (! $resolvedType && ! $this->ignorePhpDocErrors) {
throw new AnnotationException(sprintf(
'The @var annotation on %s::%s contains a non existent class "%s". '
. 'Did you maybe forget to add a "use" statement for this annotation?',
$class->name,
$property->getName(),
$type
));
}
$type = $resolvedType;
}
if (! $this->ignorePhpDocErrors && ! $this->classExists($type)) {
throw new AnnotationException(sprintf(
'The @var annotation on %s::%s contains a non existent class "%s"',
$class->name,
$property->getName(),
$type
));
}
// Remove the leading \ (FQN shouldn't contain it)
$type = is_string($type) ? ltrim($type, '\\') : null;
return $type;
}
/**
* Parse the docblock of the property to get the type (class or primitive type) of the param annotation.
*
* @return string|null Type of the property (content of var annotation)
* @throws AnnotationException
*/
public function getParameterType(ReflectionParameter $parameter): ?string
{
return $this->readParameterClass($parameter, true);
}
/**
* Parse the docblock of the property to get the class of the param annotation.
*
* @return string|null Type of the property (content of var annotation)
* @throws AnnotationException
*/
public function getParameterClass(ReflectionParameter $parameter): ?string
{
return $this->readParameterClass($parameter, false);
}
private function readParameterClass(ReflectionParameter $parameter, bool $allowPrimitiveTypes): ?string
{
// Use reflection
$parameterType = $parameter->getType();
if ($parameterType && $parameterType instanceof \ReflectionNamedType && ! $parameterType->isBuiltin()) {
return $parameterType->getName();
}
$parameterName = $parameter->name;
// Get the content of the @param annotation
$method = $parameter->getDeclaringFunction();
$docComment = $method->getDocComment();
if (! $docComment) {
return null;
}
if (preg_match('/@param\s+([^\s]+)\s+\$' . $parameterName . '/', $docComment, $matches)) {
[, $type] = $matches;
} else {
return null;
}
// Ignore primitive types
if (isset(self::PRIMITIVE_TYPES[$type])) {
if ($allowPrimitiveTypes) {
return self::PRIMITIVE_TYPES[$type];
}
return null;
}
// Ignore types containing special characters ([], <> ...)
if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
return null;
}
$class = $parameter->getDeclaringClass();
// If the class name is not fully qualified (i.e. doesn't start with a \)
if ($type[0] !== '\\') {
// Try to resolve the FQN using the class context
$resolvedType = $this->tryResolveFqn($type, $class, $parameter);
if (! $resolvedType && ! $this->ignorePhpDocErrors) {
throw new AnnotationException(sprintf(
'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
. 'Did you maybe forget to add a "use" statement for this annotation?',
$parameterName,
$class->name,
$method->name,
$type
));
}
$type = $resolvedType;
}
if (! $this->ignorePhpDocErrors && ! $this->classExists($type)) {
throw new AnnotationException(sprintf(
'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s"',
$parameterName,
$class->name,
$method->name,
$type
));
}
// Remove the leading \ (FQN shouldn't contain it)
$type = is_string($type) ? ltrim($type, '\\') : null;
return $type;
}
/**
* Attempts to resolve the FQN of the provided $type based on the $class and $member context.
*
* @return string|null Fully qualified name of the type, or null if it could not be resolved
*/
private function tryResolveFqn(string $type, ReflectionClass $class, Reflector $member): ?string
{
$alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
$loweredAlias = strtolower($alias);
// Retrieve "use" statements
$uses = $this->parser->parseUseStatements($class);
if (isset($uses[$loweredAlias])) {
// Imported classes
if ($pos !== false) {
return $uses[$loweredAlias] . substr($type, $pos);
}
return $uses[$loweredAlias];
}
if ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
return $class->getNamespaceName() . '\\' . $type;
}
if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
// Class namespace
return $uses['__NAMESPACE__'] . '\\' . $type;
}
if ($this->classExists($type)) {
// No namespace
return $type;
}
// If all fail, try resolving through related traits
return $this->tryResolveFqnInTraits($type, $class, $member);
}
/**
* Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
* through the traits that are used by the provided $class.
*
* @return string|null Fully qualified name of the type, or null if it could not be resolved
*/
private function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member): ?string
{
/** @var ReflectionClass[] $traits */
$traits = [];
// Get traits for the class and its parents
while ($class) {
$traits = array_merge($traits, $class->getTraits());
$class = $class->getParentClass();
}
foreach ($traits as $trait) {
// Eliminate traits that don't have the property/method/parameter
if ($member instanceof ReflectionProperty && ! $trait->hasProperty($member->name)) {
continue;
}
if ($member instanceof ReflectionMethod && ! $trait->hasMethod($member->name)) {
continue;
}
if ($member instanceof ReflectionParameter && ! $trait->hasMethod($member->getDeclaringFunction()->name)) {
continue;
}
// Run the resolver again with the ReflectionClass instance for the trait
$resolvedType = $this->tryResolveFqn($type, $trait, $member);
if ($resolvedType) {
return $resolvedType;
}
}
return null;
}
private function classExists(string $class): bool
{
return class_exists($class) || interface_exists($class);
}
}

View file

@ -0,0 +1,182 @@
<?php declare(strict_types=1);
namespace PhpDocReader\PhpParser;
/**
* Parses a file for namespaces/use/class declarations.
*
* Class taken and adapted from doctrine/annotations to avoid pulling the whole package.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Christian Kaps <christian.kaps@mohiva.com>
*/
class TokenParser
{
/**
* The token list.
*
* @var list<mixed[]>
*/
private $tokens;
/**
* The number of tokens.
*
* @var int
*/
private $numTokens;
/**
* The current array pointer.
*
* @var int
*/
private $pointer = 0;
/**
* @param string $contents
*/
public function __construct($contents)
{
$this->tokens = token_get_all($contents);
// The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it
// saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored
// doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a
// docblock. If the first thing in the file is a class without a doc block this would cause calls to
// getDocBlock() on said class to return our long lost doc_comment. Argh.
// To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least
// it's harmless to us.
token_get_all("<?php\n/**\n *\n */");
$this->numTokens = count($this->tokens);
}
/**
* Gets all use statements.
*
* @param string $namespaceName The namespace name of the reflected class.
*
* @return array<string, string> A list with all found use statements.
*/
public function parseUseStatements($namespaceName)
{
$statements = [];
while (($token = $this->next())) {
if ($token[0] === T_USE) {
$statements = array_merge($statements, $this->parseUseStatement());
continue;
}
if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) {
continue;
}
// Get fresh array for new namespace. This is to prevent the parser to collect the use statements
// for a previous namespace with the same name. This is the case if a namespace is defined twice
// or if a namespace with the same name is commented out.
$statements = [];
}
return $statements;
}
/**
* Gets the next non whitespace and non comment token.
*
* @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped.
* If FALSE then only whitespace and normal comments are skipped.
*
* @return mixed[]|string|null The token if exists, null otherwise.
*/
private function next($docCommentIsComment = true)
{
for ($i = $this->pointer; $i < $this->numTokens; $i++) {
$this->pointer++;
if (
$this->tokens[$i][0] === T_WHITESPACE ||
$this->tokens[$i][0] === T_COMMENT ||
($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT)
) {
continue;
}
return $this->tokens[$i];
}
return null;
}
/**
* Parses a single use statement.
*
* @return array<string, string> A list with all found class names for a use statement.
*/
private function parseUseStatement()
{
$groupRoot = '';
$class = '';
$alias = '';
$statements = [];
$explicitAlias = false;
while (($token = $this->next())) {
if (! $explicitAlias && $token[0] === T_STRING) {
$class .= $token[1];
$alias = $token[1];
} elseif ($explicitAlias && $token[0] === T_STRING) {
$alias = $token[1];
} elseif (
PHP_VERSION_ID >= 80000 &&
($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
) {
$class .= $token[1];
$classSplit = explode('\\', $token[1]);
$alias = $classSplit[count($classSplit) - 1];
} elseif ($token[0] === T_NS_SEPARATOR) {
$class .= '\\';
$alias = '';
} elseif ($token[0] === T_AS) {
$explicitAlias = true;
$alias = '';
} elseif ($token === ',') {
$statements[strtolower($alias)] = $groupRoot . $class;
$class = '';
$alias = '';
$explicitAlias = false;
} elseif ($token === ';') {
$statements[strtolower($alias)] = $groupRoot . $class;
break;
} elseif ($token === '{') {
$groupRoot = $class;
$class = '';
} elseif ($token === '}') {
continue;
} else {
break;
}
}
return $statements;
}
/**
* Gets the namespace.
*
* @return string The found namespace.
*/
private function parseNamespace()
{
$name = '';
while (
($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || (
PHP_VERSION_ID >= 80000 &&
($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
))
) {
$name .= $token[1];
}
return $name;
}
}

View file

@ -0,0 +1,64 @@
<?php declare(strict_types=1);
namespace PhpDocReader\PhpParser;
use SplFileObject;
/**
* Parses a file for "use" declarations.
*
* Class taken and adapted from doctrine/annotations to avoid pulling the whole package.
*
* Authors: Fabien Potencier <fabien@symfony.com> and Christian Kaps <christian.kaps@mohiva.com>
*/
class UseStatementParser
{
/**
* @return array A list with use statements in the form (Alias => FQN).
*/
public function parseUseStatements(\ReflectionClass $class): array
{
$filename = $class->getFilename();
if ($filename === false) {
return [];
}
$content = $this->getFileContent($filename, $class->getStartLine());
if ($content === null) {
return [];
}
$namespace = preg_quote($class->getNamespaceName(), '/');
$content = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content);
$tokenizer = new TokenParser('<?php ' . $content);
return $tokenizer->parseUseStatements($class->getNamespaceName());
}
/**
* Gets the content of the file right up to the given line number.
*
* @param string $filename The name of the file to load.
* @param int $lineNumber The number of lines to read from file.
*/
private function getFileContent(string $filename, int $lineNumber): string
{
if (! is_file($filename)) {
throw new \RuntimeException("Unable to read file $filename");
}
$content = '';
$lineCnt = 0;
$file = new SplFileObject($filename);
while (! $file->eof()) {
if ($lineCnt++ === $lineNumber) {
break;
}
$content .= $file->fgets();
}
return $content;
}
}