init
This commit is contained in:
738
instafeed/vendor/bin/lazydoctor
vendored
Executable file
738
instafeed/vendor/bin/lazydoctor
vendored
Executable file
@@ -0,0 +1,738 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/*
|
||||
* Copyright 2017 The LazyJsonMapper Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The Lazy Doctor.
|
||||
*
|
||||
* This tool automatically builds up-to-date class documentation for all classes
|
||||
* that derive from LazyJsonMapper, and documents their virtual properties and
|
||||
* functions by adding "@property" and "@method" declarations to their class
|
||||
* PHPdoc blocks. That documentation is necessary for things like code analysis
|
||||
* tools and IDE autocomplete for all of your virtual properties/functions!
|
||||
*
|
||||
* LazyDoctor also performs class diagnostics: It compiles the class property
|
||||
* maps of _all_ of your LazyJsonMapper-based classes, which means that you can
|
||||
* be 100% sure that all of your maps are valid if this tool runs successfully.
|
||||
*
|
||||
* To see all available options, type "./vendor/bin/lazydoctor --help".
|
||||
*
|
||||
* To begin, simply point the "--composer" option at the "composer.json" file
|
||||
* for your project. Just be aware that your project MUST be based on PSR-4
|
||||
* autoloading, with ONE OR MORE defined autoload-namespaces in "composer.json".
|
||||
*
|
||||
* You can read more about PSR-4 autoloading at the official Composer site:
|
||||
*
|
||||
* https://getcomposer.org/doc/04-schema.md#autoload
|
||||
*
|
||||
* This tool will ONLY parse classes that properly follow PSR-4 autoloading!
|
||||
*
|
||||
* In addition to the composer-file, you'll also have to specify whether you
|
||||
* want to document virtual "--properties", virtual "--functions", or BOTH.
|
||||
* Note that we'll ONLY document them when the individual classes support that
|
||||
* kind of access (when it hasn't disabled its class-option for that feature).
|
||||
*
|
||||
* If you don't provide any documentation flags, we'll instead REMOVE all of
|
||||
* the current class "virtual @property/@method" documentation. That can be
|
||||
* useful for restoring your files if you don't want to document them anymore.
|
||||
*
|
||||
* You should also be aware of "--document-overridden", which will document
|
||||
* virtual properties or functions even if they're already manually defined
|
||||
* (overridden) by the class or its parents. That can be useful if you want to
|
||||
* ensure that you ALWAYS document the virtual properties/functions in all of
|
||||
* your class files even when it has been overridden somewhere. In that case,
|
||||
* you should ensure that your overridden properties/functions have the SAME or
|
||||
* a COMPATIBLE signature & return value, so their auto-docs are still correct.
|
||||
*
|
||||
* Tip: You can use the "--validate-only" param to check docs without writing to
|
||||
* disk. That can be useful when making a non-destructive Git "pre-commit hook"
|
||||
* to validate your repo and ensure that all files have updated documentation!
|
||||
*
|
||||
* This script always uses exit codes to indicate success. A non-zero exit code
|
||||
* indicates that some files needed new class docs (in "--validate-only" mode),
|
||||
* or that there was a general problem while processing.
|
||||
*
|
||||
* Lastly, you may appreciate the ability to silence all unimportant status
|
||||
* messages! To do so, simply use ">/dev/null" to send STDOUT to the void...
|
||||
* That way you'll ONLY see critical status messages during the processing.
|
||||
*/
|
||||
|
||||
set_time_limit(0);
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
// Verify minimum PHP version.
|
||||
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50600) {
|
||||
fwrite(STDERR, 'LazyDoctor requires PHP 5.6 or higher.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Register a simple GetOptionKit autoloader. This is fine because
|
||||
// GetOptionKit has no 3rd party library dependencies.
|
||||
spl_autoload_register(function ($class) {
|
||||
// Check if this is a "GetOptionKit" load-request.
|
||||
static $prefix = 'GetOptionKit\\';
|
||||
static $len = 13; // strlen($prefix)
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the "GetOptionKit" source folder.
|
||||
static $dirs = [
|
||||
__DIR__.'/../../../corneltek/getoptionkit/src',
|
||||
__DIR__.'/../vendor/corneltek/getoptionkit/src',
|
||||
];
|
||||
$baseDir = null;
|
||||
foreach ($dirs as $dir) {
|
||||
if (is_dir($dir) && ($dir = realpath($dir)) !== false) {
|
||||
$baseDir = $dir;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($baseDir === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the relative class name.
|
||||
$relativeClass = substr($class, $len);
|
||||
|
||||
// Generate PSR-4 file path to the class.
|
||||
$file = sprintf('%s/%s.php', $baseDir, str_replace('\\', '/', $relativeClass));
|
||||
if (is_file($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
|
||||
// Parse command line options...
|
||||
use GetOptionKit\OptionCollection;
|
||||
use GetOptionKit\OptionParser;
|
||||
use GetOptionKit\OptionPrinter\ConsoleOptionPrinter;
|
||||
|
||||
$specs = new OptionCollection();
|
||||
$specs->add('c|composer:=file', 'Path to your project\'s composer.json file.');
|
||||
$specs->add('p|properties?=boolean', 'Document virtual properties (if enabled for the classes).');
|
||||
$specs->add('f|functions?=boolean', 'Document virtual functions (if enabled for the classes).');
|
||||
$specs->add('o|document-overridden?=boolean', 'Always document virtual functions/properties even when they have been manually overridden by the class (or its parents).');
|
||||
$specs->add('w|windows?=boolean', 'Generate Windows-style ("\r\n") documentation line endings instead of the default Unix-style ("\n").');
|
||||
$specs->add('validate-only?=boolean', 'Validate current docs for all classes but don\'t write anything to disk.');
|
||||
$specs->add('h|help?=boolean', 'Show all available options.');
|
||||
|
||||
try {
|
||||
$parser = new OptionParser($specs);
|
||||
$result = $parser->parse($argv);
|
||||
$options = [
|
||||
'composer' => isset($result->keys['composer']) ? $result->keys['composer']->value : null,
|
||||
'properties' => isset($result->keys['properties']) && $result->keys['properties']->value !== false,
|
||||
'functions' => isset($result->keys['functions']) && $result->keys['functions']->value !== false,
|
||||
'document-overridden' => isset($result->keys['document-overridden']) && $result->keys['document-overridden']->value !== false,
|
||||
'windows' => isset($result->keys['windows']) && $result->keys['windows']->value !== false,
|
||||
'validate-only' => isset($result->keys['validate-only']) && $result->keys['validate-only']->value !== false,
|
||||
'help' => isset($result->keys['help']) && $result->keys['help']->value !== false,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
// Warns in case of invalid option values.
|
||||
fwrite(STDERR, $e->getMessage().PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Verify options...
|
||||
echo '[ LazyDoctor ]'.PHP_EOL.PHP_EOL;
|
||||
if ($options['composer'] === null || $options['help']) {
|
||||
if ($options['composer'] === null) {
|
||||
fwrite(STDERR, 'You must provide the --composer option.'.PHP_EOL.PHP_EOL);
|
||||
}
|
||||
$printer = new ConsoleOptionPrinter();
|
||||
echo 'Available options:'.PHP_EOL.PHP_EOL;
|
||||
echo $printer->render($specs);
|
||||
exit($options['composer'] === null && !$options['help'] ? 1 : 0);
|
||||
}
|
||||
|
||||
if ($options['composer']->getBasename() !== 'composer.json') {
|
||||
fwrite(STDERR, 'You must point to your project\'s composer.json file.'.PHP_EOL.'You used: "'.$options['composer']->getRealPath().'".'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Decode the composer.json file...
|
||||
$json = @json_decode(file_get_contents($options['composer']->getRealPath()), true);
|
||||
if ($json === null) {
|
||||
fwrite(STDERR, 'Unable to decode composer.json.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Determine the project folder's real root path...
|
||||
$projectRoot = $options['composer']->getPathInfo()->getRealPath();
|
||||
|
||||
// Determine their namespace PSR-4 paths via their project's composer.json...
|
||||
$namespaces = [];
|
||||
foreach (['autoload', 'autoload-dev'] as $type) {
|
||||
if (!isset($json[$type]['psr-4']) || !is_array($json[$type]['psr-4'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($json[$type]['psr-4'] as $namespace => $dir) {
|
||||
// We don't support composer's empty "fallback" namespaces.
|
||||
if ($namespace === '') {
|
||||
fwrite(STDERR, 'Encountered illegal unnamed PSR-4 autoload namespace in composer.json.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Ensure that the namespace ends in backslash.
|
||||
if (substr_compare($namespace, '\\', strlen($namespace) - 1, 1) !== 0) {
|
||||
fwrite(STDERR, 'Encountered illegal namespace "'.$namespace.'" (does not end in backslash) in composer.json.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Ensure that the value is a string.
|
||||
// NOTE: We allow empty strings, which corresponds to root folder.
|
||||
if (!is_string($dir)) {
|
||||
fwrite(STDERR, 'Encountered illegal non-string value for namespace "'.$namespace.'".'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Now resolve the path name...
|
||||
$path = sprintf('%s/%s', $projectRoot, $dir);
|
||||
$realpath = realpath($path);
|
||||
if ($realpath === false) {
|
||||
fwrite(STDERR, 'Unable to resolve real path for "'.$path.'".'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// We don't allow the same directory to be defined multiple times.
|
||||
if (isset($namespaces[$realpath])) {
|
||||
fwrite(STDERR, 'Encountered duplicate namespace directory "'.$realpath.'" in composer.json.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// And we're done! The namespace and its path have been resolved.
|
||||
$namespaces[$realpath] = $namespace;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that we found some namespaces...
|
||||
if (empty($namespaces)) {
|
||||
fwrite(STDERR, 'There are no PSR-4 autoload namespaces in your composer.json.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Now load the project's autoload.php file.
|
||||
// NOTE: This is necessary so that we can autoload their classes...
|
||||
$autoload = sprintf('%s/vendor/autoload.php', $projectRoot);
|
||||
$realautoload = realpath($autoload);
|
||||
if ($realautoload === false) {
|
||||
fwrite(STDERR, 'Unable to find the project\'s Composer autoloader ("'.$autoload.'").'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
require $realautoload;
|
||||
|
||||
// Verify that their project's autoloader contains LazyJsonMapper...
|
||||
if (!class_exists('\LazyJsonMapper\LazyJsonMapper', true)) { // TRUE = Autoload.
|
||||
fwrite(STDERR, 'Target project doesn\'t contain the LazyJsonMapper library.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Alright, display the current options...
|
||||
echo 'Project: "'.$projectRoot.'".'.PHP_EOL
|
||||
.'- Documentation Line Endings: '.($options['windows'] ? 'Windows ("\r\n")' : 'Unix ("\n")').'.'.PHP_EOL
|
||||
.'- ['.($options['properties'] ? 'X' : ' ').'] Document Virtual Properties ("@property").'.PHP_EOL
|
||||
.'- ['.($options['functions'] ? 'X' : ' ').'] Document Virtual Functions ("@method").'.PHP_EOL
|
||||
.'- ['.($options['document-overridden'] ? 'X' : ' ').'] Document Overridden Properties/Functions.'.PHP_EOL;
|
||||
if ($options['validate-only']) {
|
||||
echo '- This is a validation run. Nothing will be written to disk.'.PHP_EOL;
|
||||
}
|
||||
|
||||
// We can now use our custom classes, since the autoloader has been imported...
|
||||
use LazyJsonMapper\Exception\LazyJsonMapperException;
|
||||
use LazyJsonMapper\Export\PropertyDescription;
|
||||
use LazyJsonMapper\Property\PropertyMapCache;
|
||||
use LazyJsonMapper\Property\PropertyMapCompiler;
|
||||
use LazyJsonMapper\Utilities;
|
||||
|
||||
/**
|
||||
* Automatic LazyJsonMapper-class documentation generator.
|
||||
*
|
||||
* @copyright 2017 The LazyJsonMapper Project
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0
|
||||
* @author SteveJobzniak (https://github.com/SteveJobzniak)
|
||||
*/
|
||||
class LazyClassDocumentor
|
||||
{
|
||||
/** @var PropertyMapCache */
|
||||
private static $_propertyMapCache;
|
||||
|
||||
/** @var array */
|
||||
private $_compiledPropertyMapLink;
|
||||
|
||||
/** @var ReflectionClass */
|
||||
private $_reflector;
|
||||
|
||||
/** @var array */
|
||||
private $_options;
|
||||
|
||||
/** @var string Newline sequence. */
|
||||
private $_nl;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $class
|
||||
* @param array $options
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function __construct(
|
||||
$class,
|
||||
array $options)
|
||||
{
|
||||
if (self::$_propertyMapCache === null) {
|
||||
self::$_propertyMapCache = new PropertyMapCache();
|
||||
}
|
||||
$this->_reflector = new ReflectionClass($class);
|
||||
$this->_options = $options;
|
||||
$this->_nl = $options['windows'] ? "\r\n" : "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the current class.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws LazyJsonMapperException
|
||||
*
|
||||
* @return bool `TRUE` if on-disk file has correct docs, otherwise `FALSE`.
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
// Only process user-defined classes (never any built-in PHP classes).
|
||||
if (!$this->_reflector->isUserDefined()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// There's nothing to do if this isn't a LazyJsonMapper subclass.
|
||||
// NOTE: This properly skips "\LazyJsonMapper\LazyJsonMapper" itself.
|
||||
if (!$this->_reflector->isSubclassOf('\LazyJsonMapper\LazyJsonMapper')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compile this class property map if not yet built and cached.
|
||||
$thisClassName = $this->_reflector->getName();
|
||||
if (!isset(self::$_propertyMapCache->classMaps[$thisClassName])) {
|
||||
try {
|
||||
PropertyMapCompiler::compileClassPropertyMap( // Throws.
|
||||
self::$_propertyMapCache,
|
||||
$thisClassName
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
fwrite(STDERR, '> Unable to compile the class property map for "'.$thisClassName.'". Reason: '.$e->getMessage().PHP_EOL);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now link to the property map cache for our current class.
|
||||
$this->_compiledPropertyMapLink = &self::$_propertyMapCache->classMaps[$thisClassName];
|
||||
|
||||
// Get the current class comment (string if ok, FALSE if none exists).
|
||||
$currentDocComment = $this->_reflector->getDocComment();
|
||||
if (is_string($currentDocComment)) {
|
||||
$currentDocComment = trim($currentDocComment);
|
||||
}
|
||||
|
||||
// Extract all relevant lines from the current comment.
|
||||
$finalDocLines = $this->_extractRelevantLines($currentDocComment);
|
||||
|
||||
// Generate the automatic summary line (classname followed by period).
|
||||
$autoSummaryLine = $this->_reflector->getShortName().'.';
|
||||
|
||||
// If the 1st line is a classname followed by a period, update the name.
|
||||
// NOTE: This ensures that we update all outdated auto-added classnames,
|
||||
// and the risk of false positives is very low since we only document
|
||||
// `LazyJsonMapper`-based classes with a `OneWord.`-style summary line.
|
||||
// NOTE: Regex is from http://php.net/manual/en/language.oop5.basic.php,
|
||||
// and yes we must run it in NON-UNICODE MODE, so that it parses on a
|
||||
// byte by byte basis exactly like the real PHP classname interpreter.
|
||||
if (
|
||||
isset($finalDocLines[0]) // The 1st line MUST exist to proceed.
|
||||
&& preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\.$/', $finalDocLines[0])
|
||||
) {
|
||||
$finalDocLines[0] = $autoSummaryLine;
|
||||
}
|
||||
|
||||
// Generate the magic documentation lines for the current class.
|
||||
$magicDocLines = $this->_generateMagicDocs();
|
||||
if (!empty($magicDocLines)) {
|
||||
// If there are no lines already... add the automatic summary line.
|
||||
if (empty($finalDocLines)) {
|
||||
$finalDocLines[] = $autoSummaryLine;
|
||||
}
|
||||
|
||||
// Check the 1st char of the 1st line. If it's an @tag of any kind,
|
||||
// insert automatic summary line at top and empty line after that.
|
||||
elseif ($finalDocLines[0][0] === '@') {
|
||||
array_unshift(
|
||||
$finalDocLines,
|
||||
$autoSummaryLine,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
$finalDocLines[] = ''; // Add empty line before our magic docs.
|
||||
$finalDocLines = array_merge($finalDocLines, array_values($magicDocLines));
|
||||
}
|
||||
unset($magicDocLines);
|
||||
|
||||
// Generate the final doc-comment that this class is supposed to have.
|
||||
if (!empty($finalDocLines)) {
|
||||
// This will generate even if the class only contained an existing
|
||||
// summary/tags and nothing was added by our magic handler.
|
||||
foreach ($finalDocLines as &$line) {
|
||||
$line = ($line === '' ? ' *' : " * {$line}");
|
||||
}
|
||||
unset($line);
|
||||
$finalDocComment = sprintf(
|
||||
'/**%s%s%s */',
|
||||
$this->_nl,
|
||||
implode($this->_nl, $finalDocLines),
|
||||
$this->_nl
|
||||
);
|
||||
} else {
|
||||
// The FALSE signifies that we want no class doc-block at all...
|
||||
$finalDocComment = false;
|
||||
}
|
||||
unset($finalDocLines);
|
||||
|
||||
// There's nothing to do if the doc-comment is already correct.
|
||||
// NOTE: Both values are FALSE if no doc-comment exists and none wanted.
|
||||
if ($currentDocComment === $finalDocComment) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The docs mismatch. If this is a validate-run, just return false now.
|
||||
if ($this->_options['validate-only']) {
|
||||
fwrite(STDERR, '> Outdated class docs encountered in "'.$thisClassName.'". Aborting scan...'.PHP_EOL);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the contents of the file...
|
||||
$classFileName = $this->_reflector->getFileName();
|
||||
$fileLines = @file($classFileName);
|
||||
if ($fileLines === false) {
|
||||
fwrite(STDERR, '> Unable to read class file from disk: "'.$classFileName.'".'.PHP_EOL);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split the file into lines BEFORE the class and lines AFTER the class.
|
||||
$classLine = $this->_reflector->getStartLine();
|
||||
$startLines = array_slice($fileLines, 0, $classLine - 1);
|
||||
$endLines = array_slice($fileLines, $classLine - 1);
|
||||
unset($fileLines);
|
||||
|
||||
// Insert the new class documentation using a very careful algorithm.
|
||||
if ($currentDocComment !== false) {
|
||||
// Since the class already had PHPdoc, remove it and insert new doc.
|
||||
// NOTE: A valid PHPdoc (getDocComment()) always starts with
|
||||
// "/**[whitespace]". If it's just a "/*" or something like
|
||||
// "/**Foo", then it's not detected by getDocComment(). However, the
|
||||
// comment may be several lines above the class. So we'll have to do
|
||||
// an intelligent search to find the old class-comment. As for the
|
||||
// ending tag "*/", PHP doesn't care about whitespace around that.
|
||||
// And it also doesn't let the user escape the "*/", which means
|
||||
// that if we see that sequence we KNOW it's the end of a comment!
|
||||
// NOTE: We'll search for the latest "/**[whitespace]" block and
|
||||
// remove all lines from that until its closest "*/".
|
||||
$deleteFrom = null;
|
||||
$deleteTo = null;
|
||||
for ($i = count($startLines) - 1; $i >= 0; --$i) {
|
||||
if (strpos($startLines[$i], '*/') !== false) {
|
||||
$deleteTo = $i;
|
||||
}
|
||||
if (preg_match('/^\s*\/\*\*\s/u', $startLines[$i])) {
|
||||
$deleteFrom = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we have found valid comment-offsets.
|
||||
if ($deleteFrom === null || $deleteTo === null || $deleteTo < $deleteFrom) {
|
||||
fwrite(STDERR, '> Unable to parse current class comment on disk: "'.$classFileName.'".'.PHP_EOL);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now update the startLines array to replace the doc-comment...
|
||||
foreach ($startLines as $k => $v) {
|
||||
if ($k === $deleteFrom && $finalDocComment !== false) {
|
||||
// We've found the first line of the old comment, and we
|
||||
// have a new comment. So replace that array entry.
|
||||
$startLines[$k] = $finalDocComment.$this->_nl;
|
||||
} elseif ($k >= $deleteFrom && $k <= $deleteTo) {
|
||||
// Delete all other comment lines, including the first line
|
||||
// if we had no new doc-comment.
|
||||
unset($startLines[$k]);
|
||||
}
|
||||
|
||||
// Break if we've reached the final line to delete.
|
||||
if ($k >= $deleteTo) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif ($finalDocComment !== false) {
|
||||
// There's no existing doc-comment. Just add ours above the class.
|
||||
// NOTE: This only does something if we had a new comment to insert,
|
||||
// which we SHOULD have since we came this far in this scenario...
|
||||
$startLines[] = $finalDocComment.$this->_nl;
|
||||
}
|
||||
|
||||
// Generate the new file contents.
|
||||
$newFileContent = implode($startLines).implode($endLines);
|
||||
unset($startLines);
|
||||
unset($endLines);
|
||||
|
||||
// Perform an atomic file-write to disk, which ensures that we will
|
||||
// never be able to corrupt the class-files on disk via partial writes.
|
||||
$written = Utilities::atomicWrite($classFileName, $newFileContent);
|
||||
if ($written !== false) {
|
||||
echo '> Wrote updated class documentation to disk: "'.$classFileName.'".'.PHP_EOL;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
fwrite(STDERR, '> Unable to write new class documentation to disk: "'.$classFileName.'".'.PHP_EOL);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all relevant lines from a doc-comment.
|
||||
*
|
||||
* @param string $currentDocComment
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function _extractRelevantLines(
|
||||
$currentDocComment)
|
||||
{
|
||||
if (!is_string($currentDocComment)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove the leading and trailing doc-comment tags (/** and */).
|
||||
$currentDocComment = preg_replace('/(^\s*\/\*\*\s*|\s*\*\/$)/u', '', $currentDocComment);
|
||||
|
||||
// Process all lines. Skip all @method and @property lines.
|
||||
$relevantLines = [];
|
||||
$lines = preg_split('/\r?\n|\r/u', $currentDocComment);
|
||||
foreach ($lines as $line) {
|
||||
// Remove leading and trailing whitespace, and leading asterisks.
|
||||
$line = trim(preg_replace('/^\s*\*+/u', '', $line));
|
||||
|
||||
// Skip this line if it's a @method or @property line.
|
||||
// NOTE: Removing them is totally safe, because the LazyJsonMapper
|
||||
// class has marked all of its magic property/function handlers as
|
||||
// final, which means that people's subclasses CANNOT override them
|
||||
// to add their own magic methods/properties. So therefore we KNOW
|
||||
// that ALL existing @method/@property class doc lines belong to us!
|
||||
if (preg_match('/^@(?:method|property)/u', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relevantLines[] = $line;
|
||||
}
|
||||
|
||||
// Remove trailing empty lines from the relevant lines.
|
||||
for ($i = count($relevantLines) - 1; $i >= 0; --$i) {
|
||||
if ($relevantLines[$i] === '') {
|
||||
unset($relevantLines[$i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading empty lines from the relevant lines.
|
||||
foreach ($relevantLines as $k => $v) {
|
||||
if ($v !== '') {
|
||||
break;
|
||||
}
|
||||
|
||||
unset($relevantLines[$k]);
|
||||
}
|
||||
|
||||
// Return a re-indexed (properly 0-indexed) array.
|
||||
return array_values($relevantLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PHPdoc lines for all magic properties and functions.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws LazyJsonMapperException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function _generateMagicDocs()
|
||||
{
|
||||
// Check whether we should (and can) document properties and functions.
|
||||
$documentProperties = $this->_options['properties'] && $this->_reflector->getConstant('ALLOW_VIRTUAL_PROPERTIES');
|
||||
$documentFunctions = $this->_options['functions'] && $this->_reflector->getConstant('ALLOW_VIRTUAL_FUNCTIONS');
|
||||
if (!$documentProperties && !$documentFunctions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Export all JSON properties, with RELATIVE class-paths when possible.
|
||||
// NOTE: We will document ALL properties. Even ones inherited from
|
||||
// parents/imported maps. This ensures that users who are manually
|
||||
// reading the source code can see EVERYTHING without needing an IDE.
|
||||
$properties = [];
|
||||
$ownerClassName = $this->_reflector->getName();
|
||||
foreach ($this->_compiledPropertyMapLink as $propName => $propDef) {
|
||||
$properties[$propName] = new PropertyDescription( // Throws.
|
||||
$ownerClassName,
|
||||
$propName,
|
||||
$propDef,
|
||||
true // Use relative class-paths when possible.
|
||||
);
|
||||
}
|
||||
|
||||
// Build the magic documentation...
|
||||
$magicDocLines = [];
|
||||
foreach (['functions', 'properties'] as $docType) {
|
||||
if (($docType === 'functions' && !$documentFunctions)
|
||||
|| ($docType === 'properties' && !$documentProperties)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate all lines for the current magic tag type...
|
||||
$lineStorage = [];
|
||||
foreach ($properties as $property) {
|
||||
if ($docType === 'functions') {
|
||||
// We will only document useful functions (not the "has",
|
||||
// since those are useless for properties that are fully
|
||||
// defined in the class map).
|
||||
foreach (['get', 'set', 'is', 'unset'] as $funcType) {
|
||||
// Generate the function name, ie "getSomething", and
|
||||
// skip this function if it's already defined as a REAL
|
||||
// (overridden) function in this class or its parents.
|
||||
$functionName = $funcType.$property->func_case;
|
||||
if (!$this->_options['document-overridden'] && $this->_reflector->hasMethod($functionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Alright, the function doesn't exist as a real class
|
||||
// function, or the user wants to document it anyway...
|
||||
// Document it via its calculated signature.
|
||||
// NOTE: Classtypes use paths relative to current class!
|
||||
$functionSignature = $property->{'function_'.$funcType};
|
||||
$lineStorage[$functionName] = sprintf('@method %s', $functionSignature);
|
||||
}
|
||||
} elseif ($docType === 'properties') {
|
||||
// Skip this property if it's already defined as a REAL
|
||||
// (overridden) property in this class or its parents.
|
||||
if (!$this->_options['document-overridden'] && $this->_reflector->hasProperty($property->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Alright, the property doesn't exist as a real class
|
||||
// property, or the user wants to document it anyway...
|
||||
// Document it via its calculated signature.
|
||||
// NOTE: Classtypes use paths relative to current class!
|
||||
$lineStorage[$property->name] = sprintf(
|
||||
'@property %s $%s',
|
||||
$property->type,
|
||||
$property->name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip this tag type if there was nothing to document...
|
||||
if (empty($lineStorage)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert empty line separators between different magic tag types.
|
||||
if (!empty($magicDocLines)) {
|
||||
$magicDocLines[] = '';
|
||||
}
|
||||
|
||||
// Reorder lines by name and add them to the magic doc lines.
|
||||
// NOTE: We use case sensitivity so that "getComments" and
|
||||
// "getCommentThreads" etc aren't placed next to each other.
|
||||
ksort($lineStorage, SORT_NATURAL); // Case-sensitive natural order.
|
||||
$magicDocLines = array_merge($magicDocLines, array_values($lineStorage));
|
||||
}
|
||||
|
||||
return $magicDocLines;
|
||||
}
|
||||
}
|
||||
|
||||
// Now process all PHP files under all of the project's namespace folders.
|
||||
foreach ($namespaces as $realpath => $namespace) {
|
||||
echo PHP_EOL.'Processing namespace "'.$namespace.'".'.PHP_EOL.'- Path: "'.$realpath.'".'.PHP_EOL;
|
||||
$realpathlen = strlen($realpath);
|
||||
|
||||
$iterator = new RegexIterator(
|
||||
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realpath)),
|
||||
'/\.php$/i', RecursiveRegexIterator::GET_MATCH
|
||||
);
|
||||
foreach ($iterator as $file => $ext) {
|
||||
// Determine the real path to the file (compatible with $realpath).
|
||||
$realfile = realpath($file);
|
||||
if ($realfile === false) {
|
||||
fwrite(STDERR, 'Unable to determine real path to file "'.$file.'".'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Now ensure that the file starts with the expected path...
|
||||
if (strncmp($realpath, $realfile, $realpathlen) !== 0) {
|
||||
fwrite(STDERR, 'Unexpected path to file "'.$realfile.'". Does not match project path.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
$class = substr($realfile, $realpathlen);
|
||||
|
||||
// Remove the leading slash for the folder...
|
||||
if ($class[0] !== '/' && $class[0] !== '\\') {
|
||||
fwrite(STDERR, 'Unexpected path to file "'.$realfile.'". Does not match project path.'.PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
$class = substr($class, 1);
|
||||
|
||||
// And now just generate the final class name...
|
||||
$class = sprintf(
|
||||
'%s%s',
|
||||
$namespace,
|
||||
str_replace('/', '\\', preg_replace('/\.php$/ui', '', $class))
|
||||
);
|
||||
|
||||
// Some files may not contain classes. For example, some people have
|
||||
// functions.php files with functions, etc. So before we proceed, just
|
||||
// ensure that the generated class name actually exists.
|
||||
// NOTE: class_exists() ignores interfaces. Only finds classes. Good.
|
||||
if (!class_exists($class, true)) { // TRUE = Autoload.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now process the current class.
|
||||
$documentor = new LazyClassDocumentor($class, $options);
|
||||
$result = $documentor->process();
|
||||
if (!$result) {
|
||||
if ($options['validate-only']) {
|
||||
fwrite(STDERR, '> One or more files need updated class documentation or contain other errors.'.PHP_EOL);
|
||||
} else {
|
||||
fwrite(STDERR, '> Error while processing class "'.$class.'". Aborting...'.PHP_EOL);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user