Skip to content

Instantly share code, notes, and snippets.

@jrbasso
Created July 29, 2011 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrbasso/1113866 to your computer and use it in GitHub Desktop.
Save jrbasso/1113866 to your computer and use it in GitHub Desktop.
API Doc
<?php
// Fake methods
function pluginSplit() {}
function env() {}
class ValidateAPI {
protected $_cakePath;
protected $_files = array();
protected $_cakeClasses;
protected $_messages = array();
protected $_allowedTypes = array('string', 'integer', 'float', 'mixed', 'void', 'array', 'boolean', 'resource', 'object', '[A-Z]\w+', 'stdClass');
public function __construct($cakePath = null) {
if ($cakePath) {
$this->_cakePath = $cakePath;
} else {
$this->_cakePath = getcwd() . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Cake';
}
if (!is_file($this->_cakePath . DIRECTORY_SEPARATOR . 'basics.php')) {
throw new Exception('Invalid cake folder.');
}
}
public function prepare() {
spl_autoload_register(array($this, 'autoload'));
error_reporting(error_reporting() | E_STRICT);
set_error_handler(array($this, 'errorHandler'));
$this->cakeDefinitions();
return $this;
}
public function autoload($name) {
// Hard-coded the classes that is not a filename
if ($name === 'CacheEngine') {
return include_once $this->_cakePath . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'Cache.php';
}
if ($name === 'CakeSessionHandlerInterface') {
return include_once $this->_cakePath . DIRECTORY_SEPARATOR . 'Model' . DIRECTORY_SEPARATOR . 'Datasource' . DIRECTORY_SEPARATOR . 'CakeSession.php';
}
foreach ($this->_files as $file) {
if (preg_match("/[\/\\\]{$name}\.php$/", $file)) {
return include_once $file;
}
}
return false;
}
public function errorHandler($errno, $errstr, $errfile, $errline) {
if (!(error_reporting() & $errno)) {
return;
}
switch ($errno) {
case E_USER_ERROR:
$this->out('Fatal: ' . $errstr);
exit(1);
case E_STRICT:
$this->out('Strict: ' . $errstr);
break;
default:
$this->out('Other error: ' . $errstr);
}
return true;
}
public function run() {
$this->_messages = array();
$this->_files = $this->_fileList($this->_cakePath);
$nativeClasses = get_declared_classes();
$this->_loadFiles();
$this->_cakeClasses = array_diff(get_declared_classes(), $nativeClasses);
$this->_processClasses();
$this->_processFiles();
return $this;
}
public function outputMessages() {
foreach ($this->_messages as $message) {
$this->out($message);
}
}
protected function cakeDefinitions() {
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(dirname($this->_cakePath)));
define('APP_DIR', '');
define('CAKE_CORE_INCLUDE_PATH', dirname($this->_cakePath));
define('WEBROOT_DIR', '');
define('WWW_ROOT', '');
define('APP_PATH', '');
define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
define('LOG_ERROR', 0);
}
protected function _fileList($path) {
$files = array();
$folder = new DirectoryIterator($path);
foreach ($folder as $info) {
if ($this->_isValidFile($info->getPathname())) {
$files[] = $info->getPathname();
} elseif ($info->isDir() && !$info->isDot()) {
$files = array_merge($files, $this->_fileList($info->getPathname()));
}
}
return $files;
}
protected function _loadFiles() {
foreach ($this->_files as $file) {
include_once $file;
}
}
protected function _isValidFile($file) {
if (
is_file($file) &&
pathinfo($file, PATHINFO_EXTENSION) === 'php' &&
strpos($file, 'Cake' . DS . 'Test' . DS) === false &&
strpos($file, 'Cake' . DS . 'TestSuite' . DS) === false &&
strpos($file, 'Cake' . DS . 'Console' . DS . 'Templates' . DS) === false &&
preg_match('/^[A-Z]/', pathinfo($file, PATHINFO_FILENAME))
) {
return true;
}
return false;
}
protected function _processClasses() {
foreach ($this->_cakeClasses as $cakeClass) {
$this->_processClass($cakeClass);
}
}
protected function _processClass($class) {
$refClass = new ReflectionClass($class);
$this->_checkClassPackage($refClass);
foreach ($refClass->getProperties() as $refProperty) {
if ($refProperty->class !== $class) {
continue;
}
$this->_processProperty($refProperty);
}
foreach ($refClass->getMethods() as $refMethod) {
if ($refMethod->class !== $class) {
continue;
}
$this->_processMethod($refMethod);
}
}
protected function _checkClassPackage($refClass) {
if (strpos($refClass->getDocComment(), '* @package') === false) {
$this->_messages[] = "Missing @package information on '{$refClass->name}'...";
}
}
protected function _processProperty($refProperty) {
$this->_checkPropertyVar($refProperty);
$this->_checkPropertyAccess($refProperty);
}
protected function _checkPropertyVar($refProperty) {
$doc = $refProperty->getDocComment();
// Test missing @var
if (!preg_match('/ * @var (\w+)/', $doc, $matches)) {
$this->_messages[] = "Missing @var on '{$refProperty->class}::\${$refProperty->name}'...";
return;
}
// Check if the type in in allowed list
$pattern = implode('|', $this->_allowedTypes);
$pattern = '#^(' . $pattern . ')(\|(' . $pattern . '))*$#';
if (!preg_match($pattern, $matches[1])) {
$this->_messages[] = "Property of '{$refProperty->class}::\${$refProperty->name}' ({$matches[1]}) is not allowed...";
}
}
protected function _checkPropertyAccess($refProperty) {
if (strpos($refProperty->getDocComment(), '@access') !== false) {
$this->_messages[] = "Found @access on '{$refProperty->class}::\${$refProperty->name}'...";
}
}
protected function _processMethod($refMethod) {
$this->_checkMethodParams($refMethod);
$this->_checkMethodReturn($refMethod);
$this->_checkMethodAccess($refMethod);
$this->_checkMethodStatic($refMethod);
$this->_checkMethodException($refMethod);
$this->_checkMethodVisibility($refMethod);
}
protected function _checkMethodParams($refMethod) {
$docParameters = array();
if (preg_match_all('/ * @param ([\w|]+) \$(\w+)/i', $refMethod->getDocComment(), $matches)) {
$docParameters = array_combine($matches[2], $matches[1]);
}
$methodParameters = array();
foreach ($refMethod->getParameters() as $refParameter) {
$methodParameters[$refParameter->name] = null;
if (preg_match('/\[ \<\w+\> (\w+) \$\w+ \]/', (string)$refParameter, $matches)) {
$methodParameters[$refParameter->name] = $matches[1];
}
}
// Test if method are in docs
foreach ($methodParameters as $param => $type) {
if (!isset($docParameters[$param])) {
$this->_messages[] = "Doc for parameter \${$param} not found on '{$refMethod->class}::{$refMethod->name}()'...\n";
}
}
// Test if parameters of doc exists and are in the same order of method
if (array_keys($docParameters) !== array_keys($methodParameters)) {
$this->_messages[] = "Invalid number of parameters on '{$refMethod->class}::{$refMethod->name}()'...\n" .
"\tPHP Parameters: " . implode(", ", array_keys($methodParameters)) . "\n" .
"\tDoc Parameters: " . implode(", ", array_keys($docParameters));
}
// Check code defined types
foreach ($methodParameters as $param => $type) {
if ($type !== null && isset($docParameters[$param]) && $docParameters[$param] !== $type) {
$this->_messages[] = "Invalid type on parameter {$param} to '{$refMethod->class}::{$refMethod->name}()'... Must be {$type} (defined {$docParameters[$param]}).";
}
}
// Check if the types are in allowed list
$pattern = implode('|', $this->_allowedTypes);
$pattern = '#^(' . $pattern . ')(\|(' . $pattern . '))*$#';
foreach ($docParameters as $param => $type) {
if (!preg_match($pattern, $type)) {
$this->_messages[] = "Param '{$type}' is not allowed to parameter '{$param}' on '{$refMethod->class}::{$refMethod->name}()'...";
}
}
}
protected function _checkMethodReturn($refMethod) {
$doc = $refMethod->getDocComment();
// Test return on constructor
if ($refMethod->isConstructor() || $refMethod->isDestructor()) {
if (strpos($doc, '@return') !== false) {
$this->_messages[] = "Constructor and destructor must not have @return on '{$refMethod->class}::{$refMethod->name}()'...";
}
return;
}
// Test return on methods
if (!preg_match('/ * @return (\w+)/i', $doc, $matches)) {
$this->_messages[] = "Missing @return information on '{$refMethod->class}::{$refMethod->name}()'...";
return;
}
// Check if the type in in allowed list
$pattern = implode('|', $this->_allowedTypes);
$pattern = '#^(' . $pattern . ')(\|(' . $pattern . '))*$#';
if (!preg_match($pattern, $matches[1])) {
$this->_messages[] = "Return of '{$refMethod->class}::{$refMethod->name}()' ({$matches[1]}) is not allowed...";
}
}
protected function _checkMethodAccess($refMethod) {
if (strpos($refMethod->getDocComment(), '@access') !== false) {
$this->_messages[] = "Found @access on '{$refMethod->class}::{$refMethod->name}()'...";
}
}
protected function _checkMethodStatic($refMethod) {
if (strpos($refMethod->getDocComment(), '@static') !== false) {
$this->_messages[] = "Found @access on '{$refMethod->class}::{$refMethod->name}()'...";
}
}
protected function _checkMethodException($refMethod) {
$length = $refMethod->getEndLine() - $refMethod->getStartLine();
$source = file($refMethod->getFileName());
$source = implode('', array_slice($source, $refMethod->getStartLine(), $length));
$throws = array();
if (preg_match_all('/throw new (\w+) *\(/i', $source, $matches)) {
$throws = array_unique($matches[1]);
$doc = $refMethod->getDocComment();
$throwsPos = strpos($doc, '@throws');
if ($throwsPos === false) {
$this->_messages[] = "Missing @throws on '{$refMethod->class}::{$refMethod->name}()'... Exceptions: " . implode(', ', $throws);
} else {
$throwsLine = substr($doc, $throwsPos, strpos($doc, "\n", $throwsPos) - $throwsPos);
foreach ($throws as $throw) {
if (strpos($throwsLine, $throw) === false) {
$this->_messages[] = "Missing {$throw} on @throws of '{$refMethod->class}::{$refMethod->name}()'...";
}
}
}
}
}
protected function _checkMethodVisibility($refMethod) {
$source = file($refMethod->getFileName());
$source = $source[$refMethod->getStartLine() - 1];
if (!preg_match("/^\s*(?:abstract )?(public|protected|private)( static)? function &?{$refMethod->name}\(/", $source, $matches)) {
$this->_messages[] = "Missing visibility to '{$refMethod->class}::{$refMethod->name}()'...";
return;
}
if (substr($refMethod->name, 0, 2) === '__') {
if ($matches[1] !== 'private' && !in_array($refMethod->name, array('__call', '__isset', '__get', '__set', '__construct', '__destruct', '__toString'))) {
$this->_messages[] = "Method '{$refMethod->class}::{$refMethod->name}()' must be private (now is {$matches[1]})...";
}
} elseif ($refMethod->name[0] === '_') {
if ($matches[1] !== 'protected') {
$this->_messages[] = "Method '{$refMethod->class}::{$refMethod->name}()' must be protected (now is {$matches[1]})...";
}
} elseif ($matches[1] !== 'public') {
$this->_messages[] = "Method '{$refMethod->class}::{$refMethod->name}()' must be public (now is {$matches[1]})...";
}
}
protected function _processFiles() {
foreach ($this->_files as $file) {
$content = file_get_contents($file);
$this->_checkFileLicense($file, $content);
$this->_checkFileCopyright($file, $content);
$this->_checkFileTrailingSpaces($file, $content);
}
}
protected function _checkFileLicense($file, &$content) {
$pattern = '|\* @license +MIT License \(http://www.opensource.org/licenses/mit-license\.php\)|';
if (!preg_match($pattern, $content)) {
$this->_messages[] = "Invalid license on {$file}...";
}
}
protected function _checkFileCopyright($file, &$content) {
$pattern = '|\* @copyright +Copyright 2005-2011, Cake Software Foundation, Inc\. \(http://cakefoundation\.org\)|';
if (!preg_match($pattern, $content)) {
$this->_messages[] = "Invalid copyright on {$file}...";
}
}
protected function _checkFileTrailingSpaces($file, &$content) {
foreach (explode("\n", $content) as $i => $line) {
if (preg_match('/\s+$/', $line)) {
$i++;
$this->_messages[] = "Trailing space on {$file} line {$i}...";
}
}
}
protected function out($string, $newLine = true) {
echo $string;
if ($newLine) {
echo "\n";
}
}
}
$validator = new ValidateAPI(isset($argv[1]) ? $argv[1] : null);
$validator->prepare()->run()->outputMessages();
@ADmad
Copy link

ADmad commented Aug 15, 2011

Would it be too much to add a check for trailing spaces in class files as well? That's one trivial thing that i personally find annoying :)

@jrbasso
Copy link
Author

jrbasso commented Aug 15, 2011

Yes, I can... I will include it and the visibility as we talked in the merge commit.

@jrbasso
Copy link
Author

jrbasso commented Aug 16, 2011

@ADmad, traling spaces and visibility implemented. Cake core have 150 lines with trailing in 2.0-api-doc to be fixed. I should fix in soon.

@ADmad
Copy link

ADmad commented Aug 16, 2011

Sweet

@majna
Copy link

majna commented Aug 20, 2011

You can fix class property visibility too, like

var $data;

https://github.com/cakephp/cakephp/blob/2.0/lib/Cake/Controller/Component/EmailComponent.php#L108

btw. there are many private methods which could be protected.
https://github.com/cakephp/cakephp/blob/2.0/lib/Cake/View/Helper/TimeHelper.php#L82

@ADmad
Copy link

ADmad commented Aug 20, 2011

Juan has fixed lot of visibility keywords (including the one you pointed out above) in 0575e92833646a7dd66e7400c5e41f02477ff39f and made private to protected conversion in 61833294f0c14dac3c1b6aa8845efe57bd29be99. He will probably merge them into the 2.0 branch when he's ready.

@majna
Copy link

majna commented Aug 21, 2011

oh, I didn't notice api-doc branch. Nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment