├── styles.css ├── tests ├── fixtures │ ├── phpdoc_file_required_yes2.php │ ├── phpdoc_file_required_no1.php │ ├── phpdoc_file_required_no3.php │ ├── phpdoc_file_required_no2.php │ ├── anonymous │ │ ├── anonymous.php │ │ ├── assigned.php │ │ ├── extends.php │ │ ├── implements.php │ │ ├── named.php │ │ └── extendsandimplements.php │ ├── phpdoc_file_required_yes1.php │ ├── phpdoc_file_required_yes3.php │ ├── classtags.php │ ├── constantclass.php │ ├── phpdoc_tags_test.php │ ├── phpdoc_tags_inline.php │ └── phpdoc_tags_general.php └── moodlecheck_rules_test.php ├── README.md ├── version.php ├── settings.php ├── classes └── privacy │ └── provider.php ├── index.php ├── cli └── moodlecheck.php ├── renderer.php ├── .github └── workflows │ └── ci.yml ├── lang └── en │ └── local_moodlecheck.php ├── rules ├── phpdocs_package.php └── phpdocs_basic.php ├── locallib.php └── file.php /styles.css: -------------------------------------------------------------------------------- 1 | .path-local-moodlecheck .filename, 2 | .path-local-moodlecheck .dirname { 3 | font-size: 1.1em; 4 | font-weight: bold; 5 | color: #547c22; 6 | } 7 | 8 | .path-local-moodlecheck .dirname { 9 | color: #408; 10 | } 11 | 12 | .path-local-moodlecheck ul.directory, 13 | .path-local-moodlecheck ul.file { 14 | margin: 0.5em; 15 | margin-left: 2em; 16 | } 17 | 18 | .path-local-moodlecheck ul.file { 19 | margin-top: 0; 20 | } 21 | 22 | .path-local-moodlecheck .fail { 23 | color: #600; 24 | } 25 | 26 | .path-local-moodlecheck .good { 27 | color: #060; 28 | } 29 | 30 | .path-local-moodlecheck form .fcheckbox label { 31 | margin-left: 1em; 32 | } 33 | .path-local-moodlecheck form .fcheckbox > span { 34 | margin-left: 2em; 35 | } 36 | .path-local-moodlecheck form .fradio label { 37 | margin-left: 1em; 38 | } 39 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_file_required_yes2.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | defined('MOODLE_INTERNAL') || die(); 18 | 19 | // This is library-style php file, no classes around. 20 | 21 | function dummy_filefunction_without_tags() { 22 | // No classes, hence the file phpdoc block is required. 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_file_required_no1.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | defined('MOODLE_INTERNAL') || die(); 18 | 19 | /** 20 | * This is a dummy class without any tags. 21 | */ 22 | class dummy_fileclass_without_tags { 23 | // As far as this is an 1-artifact file, the file phpdoc block is not required. 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_file_required_no3.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | defined('MOODLE_INTERNAL') || die(); 18 | 19 | /** 20 | * This is a dummy trait without any tags. 21 | */ 22 | trait dummy_filetrait_without_tags { 23 | // As far as this is an 1-artifact file, the file phpdoc block is not required. 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_file_required_no2.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | defined('MOODLE_INTERNAL') || die(); 18 | 19 | /** 20 | * This is a dummy interface without any tags. 21 | */ 22 | interface dummy_fileinterface_without_tags { 23 | // As far as this is an 1-artifact file, the file phpdoc block is not required. 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Moodle PHPdoc Checker 2 | ===================== 3 | 4 | [![Moodlecheck CI](https://github.com/moodlehq/moodle-local_moodlecheck/actions/workflows/ci.yml/badge.svg)](https://github.com/moodlehq/moodle-local_moodlecheck/actions/workflows/ci.yml) 5 | 6 | Installation: 7 | ------------- 8 | 9 | Install the source into the local/moodlecheck directory in your moodle 10 | 11 | Log in as admin and select: 12 | 13 | Settings 14 | Site administration 15 | Development 16 | Moodle PHPdoc check 17 | 18 | Enter paths to check and select rules to use. 19 | 20 | Customization: 21 | -------------- 22 | 23 | You can add new rules by adding new php files in rules/ directory, 24 | they will be included automatically. 25 | 26 | Look at other files in this directory for examples. 27 | 28 | Please note that if you register the rule with code 'mynewrule', 29 | the rule registry will look in language file for strings 30 | 'rule_mynewrule' and 'error_mynewrule'. If they are not present, 31 | the rule code will be used instead of the rule name and 32 | default error message appears. 33 | -------------------------------------------------------------------------------- /tests/fixtures/anonymous/anonymous.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 Andrew Nicols 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | return new class { 28 | }; 29 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_file_required_yes1.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | defined('MOODLE_INTERNAL') || die(); 18 | 19 | /** 20 | * This is a dummy class without any tags. 21 | */ 22 | class dummy_fileclass1_without_tags { 23 | } 24 | 25 | /** 26 | * This is another dummy class without any tags. Hence, file phpdoc block is required. 27 | */ 28 | class dummy2_fileclass2_without_tags { 29 | } 30 | -------------------------------------------------------------------------------- /tests/fixtures/anonymous/assigned.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 Andrew Nicols 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $value = new class { 28 | }; 29 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_file_required_yes3.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | defined('MOODLE_INTERNAL') || die(); 18 | 19 | /** 20 | * This is a dummy interface without any tags. 21 | */ 22 | interface dummy_fileinterface1_without_tags { 23 | } 24 | 25 | /** 26 | * This is a dummy trait without any tags. Hence, file phpdoc block is required. 27 | */ 28 | trait dummy_filetrait1_without_tags { 29 | } 30 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Version details. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | $plugin->version = 2021051200; 28 | $plugin->requires = 2018051700; 29 | $plugin->component = 'local_moodlecheck'; 30 | -------------------------------------------------------------------------------- /tests/fixtures/anonymous/extends.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 Andrew Nicols 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | return new class extends parentclass { 28 | }; 29 | -------------------------------------------------------------------------------- /tests/fixtures/anonymous/implements.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 Andrew Nicols 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | return new class implements someinterface { 28 | }; 29 | -------------------------------------------------------------------------------- /tests/fixtures/anonymous/named.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 Andrew Nicols 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace local_moodlecheck\test\fixtures\anonymous; 26 | 27 | class someclass extends parentclass { 28 | } 29 | -------------------------------------------------------------------------------- /tests/fixtures/anonymous/extendsandimplements.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 Andrew Nicols 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | return new class extends parentclass implements someinterface { 28 | }; 29 | -------------------------------------------------------------------------------- /tests/fixtures/classtags.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Fixture file providing a class. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2018 David Mudrák 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * This is a dummy class without any tags. 29 | */ 30 | class dummy_class_without_tags { 31 | } 32 | -------------------------------------------------------------------------------- /settings.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Add page to admin menu. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | if ($hassiteconfig) { // Needs this condition or there is error on login page. 28 | $ADMIN->add('development', new admin_externalpage('local_moodlecheck', 29 | get_string('pluginname', 'local_moodlecheck'), 30 | new moodle_url('/local/moodlecheck/index.php'))); 31 | } 32 | -------------------------------------------------------------------------------- /tests/fixtures/constantclass.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Unit tests for a fixture file in moodlecheck. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | global $CFG; 28 | 29 | /** 30 | * Unit tests for a fixture class in moodlecheck. 31 | * 32 | * @package local_moodlecheck 33 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class fixturing_classconstant extends advanced_testcase { 37 | 38 | /** 39 | * Fixture method 40 | */ 41 | public function test_fixtured() { 42 | $this->assertInstanceOf(\stdClass::class, $result); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /classes/privacy/provider.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Privacy provider implementation for local_moodlecheck 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2019 Paul Holden 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace local_moodlecheck\privacy; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * Privacy provider for local_moodlecheck implementing null provider 31 | * 32 | * @package local_moodlecheck 33 | * @copyright 2019 Paul Holden 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class provider implements \core_privacy\local\metadata\null_provider { 37 | 38 | /** 39 | * Get the language string identifier with the component's language 40 | * file to explain why this plugin stores no data. 41 | * 42 | * @return string 43 | */ 44 | public static function get_reason(): string { 45 | return 'privacy:metadata'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_tags_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * A fixture to verify various phpdoc tags in a tests location. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | global $CFG; 28 | 29 | /** 30 | * A fixture to verify various phpdoc tags in a tests location. 31 | * 32 | * @package local_moodlecheck 33 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class fixturing_tests { 37 | 38 | /** 39 | * Some valid tags, to verify they are ok. 40 | * 41 | * @license 42 | * @throws 43 | * @deprecated 44 | * @author 45 | * @todo 46 | */ 47 | public function all_valid_tags() { 48 | echo "yay!"; 49 | } 50 | 51 | /** 52 | * Some more valid tags, because we are under tests area. 53 | * 54 | * @covers 55 | * @dataProvider 56 | * @group 57 | * @runTestsInSeparateProcesses 58 | */ 59 | public function also_all_valid_tags() { 60 | echo "reyay!"; 61 | } 62 | 63 | /** 64 | * Some invalid tags. 65 | * 66 | * @small 67 | * @zzzing 68 | * @inheritdoc 69 | */ 70 | public function all_invalid_tags() { 71 | echo "yoy!"; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_tags_inline.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * A fixture to verify various phpdoc tags are allowed inline. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2020 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | global $CFG; 28 | 29 | /** 30 | * A fixture to verify various phpdoc tags can be used standalone or inline. 31 | * 32 | * @package local_moodlecheck 33 | * @copyright 2020 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class fixturing_inline { 37 | 38 | /** 39 | * Some valid tags, to verify they are ok. 40 | * 41 | * @license 42 | * @throws 43 | * @deprecated 44 | * @author 45 | * @todo 46 | * @uses 47 | */ 48 | public function all_valid_tags() { 49 | echo "yay!"; 50 | } 51 | 52 | /** 53 | * Some tags that are valid standalone and inline 54 | * 55 | * @link https://moodle.org 56 | * @see has_capability() 57 | * 58 | * To know more, visit {@link https://moodle.org} for Moodle info. 59 | * To know more, take a look to {@see has_capability} about permissions. 60 | * And verify that crazy {@see \so-me\com-plex\th_ing::$come->ba8by()} are ok too. 61 | */ 62 | public function correct_inline_tags() { 63 | echo "done!"; 64 | } 65 | 66 | /** 67 | * Some invalid inline tags. 68 | * 69 | * This tag {@param string Some param} cannot be used inline. 70 | * Neither {@throws exception} can. 71 | * Ideally all {@link tags have to be 1 url}. 72 | * And all {@see must be 1 word only} 73 | * And {@see $this->tagrules['url']} is not a proper structural element. 74 | * Also they aren't valid without using @see curly brackets 75 | */ 76 | public function all_invalid_tags() { 77 | echo "done!"; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Main interface to Moodle PHP code check 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once('../../config.php'); 26 | require_once($CFG->dirroot. '/local/moodlecheck/locallib.php'); 27 | 28 | // Include all files from rules directory. 29 | if ($dh = opendir($CFG->dirroot. '/local/moodlecheck/rules')) { 30 | while (($file = readdir($dh)) !== false) { 31 | if ($file != '.' && $file != '..') { 32 | $pathinfo = pathinfo($file); 33 | if (isset($pathinfo['extension']) && $pathinfo['extension'] == 'php') { 34 | require_once($CFG->dirroot. '/local/moodlecheck/rules/'. $file); 35 | } 36 | } 37 | } 38 | closedir($dh); 39 | } 40 | 41 | require_login(); 42 | $context = context_system::instance(); 43 | $PAGE->set_context($context); 44 | $PAGE->set_pagelayout('admin'); 45 | $PAGE->set_heading($SITE->fullname); 46 | $PAGE->set_title($SITE->fullname . ': ' . get_string('pluginname', 'local_moodlecheck')); 47 | $PAGE->set_url(new moodle_url('/local/moodlecheck/index.php')); 48 | $output = $PAGE->get_renderer('local_moodlecheck'); 49 | 50 | echo $output->header(); 51 | 52 | $form = new local_moodlecheck_form(); 53 | $form->display(); 54 | 55 | if ($form->is_submitted() && $form->is_validated()) { 56 | $data = $form->get_data(); 57 | $paths = preg_split('/\s*\n\s*/', trim($data->path), null, PREG_SPLIT_NO_EMPTY); 58 | $ignorepaths = preg_split('/\s*\n\s*/', trim($data->ignorepath), null, PREG_SPLIT_NO_EMPTY); 59 | if (isset($data->checkall) && $data->checkall == 'selected' && isset($data->rule)) { 60 | foreach ($data->rule as $code => $value) { 61 | local_moodlecheck_registry::enable_rule($code); 62 | } 63 | } else { 64 | local_moodlecheck_registry::enable_all_rules(); 65 | } 66 | 67 | // Store result for later output. 68 | $result = []; 69 | 70 | foreach ($paths as $filename) { 71 | $path = new local_moodlecheck_path($filename, $ignorepaths); 72 | $result[] = $output->display_path($path); 73 | } 74 | 75 | echo $output->display_summary(); 76 | 77 | foreach ($result as $line) { 78 | echo $line; 79 | } 80 | } 81 | 82 | echo $output->footer(); 83 | -------------------------------------------------------------------------------- /cli/moodlecheck.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Command-line script to Moodle PHP code check 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | define('CLI_SCRIPT', true); 26 | 27 | require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php'); 28 | require_once($CFG->libdir.'/clilib.php'); // CLI only functions. 29 | require_once($CFG->dirroot. '/local/moodlecheck/locallib.php'); 30 | 31 | // Now get cli options. 32 | list($options, $unrecognized) = cli_get_params( 33 | array('help' => false, 'path' => '', 'format' => 'xml', 'exclude' => '', 'rules' => 'all', 'componentsfile' => ''), 34 | array('h' => 'help', 'p' => 'path', 'f' => 'format', 'e' => 'exclude', 'r' => 'rules', 'c' => 'componentsfile') 35 | ); 36 | 37 | $rules = preg_split('/\s*[\n,;]\s*/', trim($options['rules']), null, PREG_SPLIT_NO_EMPTY); 38 | $paths = preg_split('/\s*[\n,;]\s*/', trim($options['path']), null, PREG_SPLIT_NO_EMPTY); 39 | $exclude = preg_split('/\s*[\n,;]\s*/', trim($options['exclude']), null, PREG_SPLIT_NO_EMPTY); 40 | if (!in_array($options['format'], array('xml', 'html', 'text'))) { 41 | unset($options['format']); 42 | } 43 | 44 | if ($options['help'] || !isset($options['format']) || !count($paths)) { 45 | $help = "Perform Moodle PHP code check. 46 | 47 | This script checks all files found in the specified paths against defined rules 48 | 49 | Options: 50 | -h, --help Print out this help 51 | -p, --path Path(s) to check. Specify paths from the root directory, 52 | separate multiple paths with comman, semicolon or newline 53 | -e, --exclude Path(s) or files to be excluded. Non-php files are 54 | automatically excluded 55 | -r, --rules List rules to check against. Default 'all' 56 | -f, --format Output format: html, xml, text. Default 'xml' 57 | -c, --componentsfile Path to one file contaning the list of valid components in format: type, name, fullpath 58 | 59 | Example: 60 | \$sudo -u www-data /usr/bin/php local/moodlecheck/cli/moodlecheck.php -p=local/moodlecheck 61 | "; 62 | 63 | echo $help; 64 | die; 65 | } 66 | 67 | // Include all files from rules directory. 68 | if ($dh = opendir($CFG->dirroot. '/local/moodlecheck/rules')) { 69 | while (($file = readdir($dh)) !== false) { 70 | if ($file != '.' && $file != '..') { 71 | $pathinfo = pathinfo($file); 72 | if (isset($pathinfo['extension']) && $pathinfo['extension'] == 'php') { 73 | require_once($CFG->dirroot. '/local/moodlecheck/rules/'. $file); 74 | } 75 | } 76 | } 77 | closedir($dh); 78 | } 79 | 80 | $output = $PAGE->get_renderer('local_moodlecheck'); 81 | 82 | if (count($rules) && !in_array('all', $rules)) { 83 | foreach ($rules as $code) { 84 | local_moodlecheck_registry::enable_rule($code); 85 | } 86 | } else { 87 | local_moodlecheck_registry::enable_all_rules(); 88 | } 89 | foreach ($paths as $filename) { 90 | $path = new local_moodlecheck_path($filename, $exclude); 91 | local_moodlecheck_path::get_components($options['componentsfile']); 92 | echo $output->display_path($path, $options['format']); 93 | } 94 | -------------------------------------------------------------------------------- /renderer.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Renderer for local_moodlecheck 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | /** 28 | * Renderer for displaying local_moodlecheck 29 | * 30 | * @package local_moodlecheck 31 | * @copyright 2012 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class local_moodlecheck_renderer extends plugin_renderer_base { 35 | 36 | /** @var int $errorcount */ 37 | protected $errorcount = 0; 38 | 39 | /** 40 | * Generates html to display one path validation results (invoked recursively) 41 | * 42 | * @param local_moodlecheck_path $path 43 | * @param string $format display format: html, xml, text 44 | * @return string 45 | */ 46 | public function display_path(local_moodlecheck_path $path, $format = 'html') { 47 | $output = ''; 48 | $path->validate(); 49 | if ($path->is_dir()) { 50 | if ($format == 'html') { 51 | $output .= html_writer::start_tag('li', array('class' => 'directory')); 52 | $output .= html_writer::tag('span', $path->get_path(), array('class' => 'dirname')); 53 | $output .= html_writer::start_tag('ul', array('class' => 'directory')); 54 | } else if ($format == 'xml' && $path->is_rootpath()) { 55 | // Insert XML preamble and root element. 56 | $output .= '' . PHP_EOL . 57 | '' . PHP_EOL; 58 | } 59 | foreach ($path->get_subpaths() as $subpath) { 60 | $output .= $this->display_path($subpath, $format); 61 | } 62 | if ($format == 'html') { 63 | $output .= html_writer::end_tag('li'); 64 | $output .= html_writer::end_tag('ul'); 65 | } else if ($format == 'xml' && $path->is_rootpath()) { 66 | // Close root element. 67 | $output .= ''; 68 | } 69 | } else if ($path->is_file() && $path->get_file()->needs_validation()) { 70 | $output .= $this->display_file_validation($path->get_path(), $path->get_file(), $format); 71 | } 72 | return $output; 73 | } 74 | 75 | /** 76 | * Generates html to display one file validation results 77 | * 78 | * @param string $filename 79 | * @param local_moodlecheck_file $file 80 | * @param string $format display format: html, xml, text 81 | * @return string 82 | */ 83 | public function display_file_validation($filename, local_moodlecheck_file $file, $format = 'html') { 84 | $output = ''; 85 | $errors = $file->validate(); 86 | $this->errorcount += count($errors); 87 | if ($format == 'html') { 88 | $output .= html_writer::start_tag('li', array('class' => 'file')); 89 | $output .= html_writer::tag('span', $filename, array('class' => 'filename')); 90 | $output .= html_writer::start_tag('ul', array('class' => 'file')); 91 | } else if ($format == 'xml') { 92 | $output .= html_writer::start_tag('file', array('name' => $filename)). "\n"; 93 | } else if ($format == 'text') { 94 | $output .= $filename. "\n"; 95 | } 96 | foreach ($errors as $error) { 97 | if (($format == 'html' || $format == 'text') && isset($error['line']) && strlen($error['line'])) { 98 | $error['message'] = get_string('linenum', 'local_moodlecheck', $error['line']). $error['message']; 99 | } 100 | if ($format == 'html') { 101 | $output .= html_writer::tag('li', $error['message'], array('class' => 'errorline')); 102 | } else { 103 | $error['message'] = strip_tags($error['message']); 104 | if ($format == 'text') { 105 | $output .= " ". $error['message']. "\n"; 106 | } else if ($format == 'xml') { 107 | $output .= ' '.html_writer::empty_tag('error', $error). "\n"; 108 | } 109 | } 110 | } 111 | if ($format == 'html') { 112 | $output .= html_writer::end_tag('ul'); 113 | $output .= html_writer::end_tag('li'); 114 | } else if ($format == 'xml') { 115 | $output .= html_writer::end_tag('file'). "\n"; 116 | } 117 | return $output; 118 | } 119 | 120 | /** 121 | * Display report summary 122 | * 123 | * @return string 124 | */ 125 | public function display_summary() { 126 | if ($this->errorcount > 0) { 127 | return html_writer::tag('h2', get_string('notificationerror', 'local_moodlecheck', $this->errorcount), 128 | ['class' => 'fail']); 129 | } else { 130 | return html_writer::tag('h2', get_string('notificationsuccess', 'local_moodlecheck'), ['class' => 'good']); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Moodlecheck CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-18.04 8 | 9 | services: 10 | postgres: 11 | image: postgres:10 12 | env: 13 | POSTGRES_USER: 'postgres' 14 | POSTGRES_HOST_AUTH_METHOD: 'trust' 15 | ports: 16 | - 5432:5432 17 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 18 | mariadb: 19 | image: mariadb:10.5 20 | env: 21 | MYSQL_USER: 'root' 22 | MYSQL_ALLOW_EMPTY_PASSWORD: "true" 23 | ports: 24 | - 3306:3306 25 | options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - php: 8.0 32 | moodle-branch: master 33 | database: pgsql 34 | # The following line is only needed if you're going to run php8 jobs and your 35 | # plugin needs xmlrpc services. 36 | extensions: xmlrpc-beta 37 | - php: 8.0 38 | moodle-branch: master 39 | database: mariadb 40 | # The following line is only needed if you're going to run php8 jobs and your 41 | # plugin needs xmlrpc services. 42 | extensions: xmlrpc-beta 43 | - php: 8.0 44 | moodle-branch: MOODLE_311_STABLE 45 | database: pgsql 46 | # The following line is only needed if you're going to run php8 jobs and your 47 | # plugin needs xmlrpc services. 48 | extensions: xmlrpc-beta 49 | - php: 8.0 50 | moodle-branch: MOODLE_311_STABLE 51 | database: mariadb 52 | # The following line is only needed if you're going to run php8 jobs and your 53 | # plugin needs xmlrpc services. 54 | extensions: xmlrpc-beta 55 | 56 | - php: 7.4 57 | moodle-branch: MOODLE_310_STABLE 58 | database: pgsql 59 | - php: 7.4 60 | moodle-branch: MOODLE_39_STABLE 61 | database: mariadb 62 | - php: 7.4 63 | moodle-branch: MOODLE_38_STABLE 64 | database: pgsql 65 | 66 | - php: 7.3 67 | moodle-branch: master 68 | database: pgsql 69 | - php: 7.3 70 | moodle-branch: master 71 | database: mariadb 72 | - php: 7.3 73 | moodle-branch: MOODLE_311_STABLE 74 | database: pgsql 75 | - php: 7.3 76 | moodle-branch: MOODLE_311_STABLE 77 | database: mariadb 78 | - php: 7.3 79 | moodle-branch: MOODLE_37_STABLE 80 | database: pgsql 81 | 82 | - php: 7.2 83 | moodle-branch: MOODLE_310_STABLE 84 | database: pgsql 85 | - php: 7.2 86 | moodle-branch: MOODLE_39_STABLE 87 | database: mariadb 88 | 89 | - php: 7.1 90 | moodle-branch: MOODLE_38_STABLE 91 | database: pgsql 92 | - php: 7.1 93 | moodle-branch: MOODLE_37_STABLE 94 | database: mariadb 95 | steps: 96 | - name: Check out repository code 97 | uses: actions/checkout@v2 98 | with: 99 | path: plugin 100 | 101 | - name: Setup PHP ${{ matrix.php }} 102 | uses: shivammathur/setup-php@v2 103 | with: 104 | php-version: ${{ matrix.php }} 105 | extensions: ${{ matrix.extensions }} 106 | ini-values: max_input_vars=5000 107 | coverage: none 108 | 109 | - name: Initialise moodle-plugin-ci 110 | run: | 111 | composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 112 | echo $(cd ci/bin; pwd) >> $GITHUB_PATH 113 | echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH 114 | sudo locale-gen en_AU.UTF-8 115 | echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV 116 | 117 | - name: Install moodle-plugin-ci 118 | run: | 119 | moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 120 | env: 121 | DB: ${{ matrix.database }} 122 | MOODLE_BRANCH: ${{ matrix.moodle-branch }} 123 | IGNORE_PATHS: moodle/tests/fixtures,moodle/Sniffs 124 | 125 | - name: PHP Lint 126 | if: ${{ always() }} 127 | run: moodle-plugin-ci phplint 128 | 129 | - name: PHP Copy/Paste Detector 130 | continue-on-error: true # This step will show errors but will not fail 131 | if: ${{ always() }} 132 | run: moodle-plugin-ci phpcpd 133 | 134 | - name: PHP Mess Detector 135 | continue-on-error: true # This step will show errors but will not fail 136 | if: ${{ always() }} 137 | run: moodle-plugin-ci phpmd 138 | 139 | - name: Moodle Code Checker 140 | if: ${{ always() }} 141 | run: moodle-plugin-ci codechecker --max-warnings 0 142 | 143 | - name: Moodle PHPDoc Checker 144 | if: ${{ false }} 145 | run: moodle-plugin-ci phpdoc 146 | 147 | - name: Validating 148 | if: ${{ always() }} 149 | run: moodle-plugin-ci validate 150 | 151 | - name: Check upgrade savepoints 152 | if: ${{ always() }} 153 | run: moodle-plugin-ci savepoints 154 | 155 | - name: Mustache Lint 156 | if: ${{ always() }} 157 | run: moodle-plugin-ci mustache 158 | 159 | - name: Grunt 160 | if: ${{ always() }} 161 | run: moodle-plugin-ci grunt --max-lint-warnings 0 162 | 163 | - name: PHPUnit tests 164 | if: ${{ always() }} 165 | run: moodle-plugin-ci phpunit 166 | 167 | - name: Behat features 168 | if: ${{ always() }} 169 | run: moodle-plugin-ci behat --profile chrome 170 | -------------------------------------------------------------------------------- /lang/en/local_moodlecheck.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Strings for local_moodlecheck 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | $string['pluginname'] = 'Moodle PHPdoc check'; 26 | $string['path'] = 'Path(s)'; 27 | $string['ignorepath'] = 'Subpaths to ignore'; 28 | $string['path_help'] = 'Specify one or more files and/or directories to check as local paths from Moodle installation directory'; 29 | $string['check'] = 'Check'; 30 | $string['checkallrules'] = 'Check over all rules'; 31 | $string['checkselectedrules'] = 'Check over selected rules'; 32 | $string['error_default'] = 'Error: {$a}'; 33 | $string['linenum'] = 'Line {$a}: '; 34 | $string['notificationerror'] = 'Found {$a} errors'; 35 | $string['notificationsuccess'] = 'Well done!'; 36 | $string['privacy:metadata'] = 'The Moodle PHPdoc check plugin does not store any personal data.'; 37 | 38 | $string['rule_noemptysecondline'] = 'Php open tag in the first line is not followed by empty line'; 39 | $string['error_noemptysecondline'] = 'Empty line found after PHP open tag'; 40 | 41 | $string['rule_filephpdocpresent'] = 'File-level phpdocs block is present'; 42 | $string['error_filephpdocpresent'] = 'File-level phpdocs block is not found'; 43 | 44 | $string['rule_classesdocumented'] = 'All classes are documented'; 45 | $string['error_classesdocumented'] = 'Class {$a->class} is not documented'; 46 | $string['rule_functionsdocumented'] = 'All functions are documented'; 47 | $string['error_functionsdocumented'] = 'Function {$a->function} is not documented'; 48 | $string['rule_variablesdocumented'] = 'All variables are documented'; 49 | $string['error_variablesdocumented'] = 'Variable {$a->variable} is not documented'; 50 | $string['rule_constsdocumented'] = 'All constants are documented'; 51 | $string['error_constsdocumented'] = 'Constant {$a->object} is not documented'; 52 | $string['rule_definesdocumented'] = 'All define statements are documented'; 53 | $string['error_definesdocumented'] = 'Define statement for {$a->object} is not documented'; 54 | 55 | $string['rule_noinlinephpdocs'] = 'There are no comments starting with three or more slashes'; 56 | $string['error_noinlinephpdocs'] = 'Found comment starting with three or more slashes'; 57 | 58 | $string['error_phpdocsfistline'] = 'No one-line description found in phpdocs for {$a->object}'; 59 | $string['rule_phpdocsfistline'] = 'File-level phpdocs block and class phpdocs should have one-line short description'; 60 | 61 | $string['error_phpdocsinvalidtag'] = 'Invalid phpdocs tag {$a->tag} used'; 62 | $string['rule_phpdocsinvalidtag'] = 'Used phpdocs tags are valid'; 63 | 64 | $string['error_phpdocsnotrecommendedtag'] = 'Not recommended phpdocs tag {$a->tag} used'; 65 | $string['rule_phpdocsnotrecommendedtag'] = 'Used phpdocs tags are recommended'; 66 | 67 | $string['error_phpdocsinvalidpathtag'] = 'Incorrect path for phpdocs tag {$a->tag} detected'; 68 | $string['rule_phpdocsinvalidpathtag'] = 'Used phpdocs tags have correct paths'; 69 | 70 | $string['error_phpdocsinvalidinlinetag'] = 'Invalid inline phpdocs tag {$a->tag} found'; 71 | $string['rule_phpdocsinvalidinlinetag'] = 'Inline phpdocs tags are valid'; 72 | 73 | $string['error_phpdocsuncurlyinlinetag'] = 'Inline phpdocs tag not enclosed with curly brackets {$a->tag} found'; 74 | $string['rule_phpdocsuncurlyinlinetag'] = 'Inline phpdocs tags are enclosed with curly brackets'; 75 | 76 | $string['error_phpdoccontentsinlinetag'] = 'Inline phpdocs tag {$a->tag} with incorrect contents found. It must match {@link valid URL} or {@see valid FQSEN}'; 77 | $string['rule_phpdoccontentsinlinetag'] = 'Inline phpdocs tags have correct contents'; 78 | 79 | $string['error_functiondescription'] = 'There is no description in phpdocs for function {$a->object}'; 80 | $string['rule_functiondescription'] = 'Functions have descriptions in phpdocs'; 81 | 82 | $string['error_functionarguments'] = 'Phpdocs for function {$a->function} has incomplete parameters list'; 83 | $string['rule_functionarguments'] = 'Phpdocs for functions properly define all parameters'; 84 | 85 | $string['error_variableshasvar'] = 'Phpdocs for variable {$a->variable} does not contain @var or incorrect'; 86 | $string['rule_variableshasvar'] = 'Phpdocs for variables contain @var with variable type and name'; 87 | 88 | $string['error_definedoccorrect'] = 'Phpdocs for define statement must start with constant name and dash: {$a->object}'; 89 | $string['rule_definedoccorrect'] = 'Check syntax for define statement'; 90 | 91 | $string['error_packagespecified'] = 'Package is not specified for {$a->object}. It is also not specified in file-level phpdocs'; 92 | $string['rule_packagespecified'] = 'All functions (which are not methods) and classes have package specified or inherited'; 93 | 94 | $string['rule_packagevalid'] = 'Package tag is valid'; 95 | $string['error_packagevalid'] = 'Package {$a->package} is not valid'; 96 | 97 | $string['rule_categoryvalid'] = 'Category tag is valid'; 98 | $string['error_categoryvalid'] = 'Category {$a->category} is not valid'; 99 | 100 | $string['rule_filehascopyright'] = 'Files have @copyright tag'; 101 | $string['error_filehascopyright'] = 'File-level phpdocs block does not have @copyright tag'; 102 | 103 | $string['rule_filehaslicense'] = 'Files have @license tag'; 104 | $string['error_filehaslicense'] = 'File-level phpdocs block does not have @license tag'; 105 | -------------------------------------------------------------------------------- /tests/fixtures/phpdoc_tags_general.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * A fixture to verify various phpdoc tags in a general location. 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | global $CFG; 28 | 29 | /** 30 | * A fixture to verify various phpdoc tags in a general location. 31 | * 32 | * @package local_moodlecheck 33 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class fixturing_general { 37 | 38 | /** 39 | * Some valid tags, to verify they are ok. 40 | * 41 | * @license 42 | * @throws 43 | * @deprecated 44 | * @author 45 | * @todo 46 | */ 47 | public function all_valid_tags() { 48 | echo "yay!"; 49 | } 50 | 51 | /** 52 | * Some invalid tags, to verify they are detected. 53 | * 54 | * @codingStandardsIgnoreLine 55 | * @covers 56 | * @dataProvider 57 | * @group 58 | * @small 59 | * @zzzing 60 | * @inheritdoc 61 | */ 62 | public function all_invalid_tags() { 63 | echo "yoy!"; 64 | } 65 | 66 | /** 67 | * Incomplete param annotation (type is missing). 68 | * 69 | * @param $one 70 | * @param $two 71 | */ 72 | public function incomplete_param_annotation($one, $two) { 73 | echo "yoy!"; 74 | } 75 | 76 | /** 77 | * Missing param definition. 78 | * 79 | * @param string $one 80 | * @param bool $two 81 | */ 82 | public function missing_param_defintion() { 83 | echo "yoy!"; 84 | } 85 | 86 | /** 87 | * Missing param annotation. 88 | */ 89 | public function missing_param_annotation(string $one, bool $two) { 90 | echo "yoy!"; 91 | } 92 | 93 | /** 94 | * Incomplete param definition. 95 | * 96 | * @param string $one 97 | * @param bool $two 98 | */ 99 | public function incomplete_param_definition(string $one) { 100 | echo "yoy!"; 101 | } 102 | 103 | /** 104 | * Incomplete param annotation (annotation is missing). 105 | * 106 | * @param string $one 107 | */ 108 | public function incomplete_param_annotation1(string $one, bool $two) { 109 | echo "yoy!"; 110 | } 111 | 112 | /** 113 | * Mismatch param types. 114 | * 115 | * @param string $one 116 | * @param bool $two 117 | */ 118 | public function mismatch_param_types(string $one, array $two = []) { 119 | echo "yoy!"; 120 | } 121 | 122 | /** 123 | * Mismatch param types. 124 | * 125 | * @param string|bool $one 126 | * @param bool $two 127 | */ 128 | public function mismatch_param_types1(string $one, bool $two) { 129 | echo "yoy!"; 130 | } 131 | 132 | /** 133 | * Mismatch param types. 134 | * 135 | * @param string|bool $one 136 | * @param bool $params 137 | */ 138 | public function mismatch_param_types2(string $one, ...$params) { 139 | echo "yoy!"; 140 | } 141 | 142 | /** 143 | * Mismatch param types. 144 | * 145 | * @param string $one 146 | * @param int[] $params 147 | */ 148 | public function mismatch_param_types3(string $one, int $params) { 149 | echo "yoy!"; 150 | } 151 | 152 | /** 153 | * Correct param types. 154 | * 155 | * @param string|bool $one 156 | * @param bool $two 157 | * @param array $three 158 | */ 159 | public function correct_param_types($one, bool $two, array $three) { 160 | echo "yay!"; 161 | } 162 | 163 | /** 164 | * Correct param types. 165 | * 166 | * @param string|bool $one 167 | * @param bool $two 168 | * @param array $three 169 | */ 170 | public function correct_param_types1($one, bool $two, array $three) { 171 | echo "yay!"; 172 | } 173 | 174 | /** 175 | * Correct param types. 176 | * 177 | * @param string $one 178 | * @param bool $two 179 | */ 180 | public function correct_param_types2($one, $two) { 181 | echo "yay!"; 182 | } 183 | 184 | /** 185 | * Correct param types. 186 | * 187 | * @param string|null $one 188 | * @param bool $two 189 | * @param array $three 190 | */ 191 | public function correct_param_types3(?string $one = null, bool $two, array $three) { 192 | echo "yay!"; 193 | } 194 | 195 | /** 196 | * Correct param types. 197 | * 198 | * @param string|null $one 199 | * @param bool $two 200 | * @param int[] $three 201 | */ 202 | public function correct_param_types4($one = null, bool $two, array $three) { 203 | echo "yay!"; 204 | } 205 | 206 | /** 207 | * Correct param types. 208 | * 209 | * @param string $one 210 | * @param mixed ...$params one or more params 211 | */ 212 | public function correct_param_types5(string $one, ...$params) { 213 | echo "yay!"; 214 | } 215 | 216 | /** 217 | * Incomplete return annotation (type is missing). 218 | * 219 | * @return 220 | */ 221 | public function incomplete_return_annotation() { 222 | echo "yoy!"; 223 | } 224 | 225 | /** 226 | * Correct return type. 227 | * 228 | * @return string 229 | */ 230 | public function correct_return_type(): string { 231 | return "yay!"; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /rules/phpdocs_package.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Registering rules for checking phpdocs related to package and category tags 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | local_moodlecheck_registry::add_rule('packagespecified')->set_callback('local_moodlecheck_packagespecified'); 28 | local_moodlecheck_registry::add_rule('packagevalid')->set_callback('local_moodlecheck_packagevalid'); 29 | local_moodlecheck_registry::add_rule('categoryvalid')->set_callback('local_moodlecheck_categoryvalid'); 30 | 31 | /** 32 | * Checks if all functions (outside class) and classes have package 33 | * 34 | * package tag may be inherited from file-level phpdocs 35 | * 36 | * @param local_moodlecheck_file $file 37 | * @return array of found errors 38 | */ 39 | function local_moodlecheck_packagespecified(local_moodlecheck_file $file) { 40 | $errors = array(); 41 | $phpdocs = $file->find_file_phpdocs(); 42 | if ($phpdocs && count($phpdocs->get_tags('package', true))) { 43 | // Package is specified on file level, it is automatically inherited. 44 | return array(); 45 | } 46 | foreach ($file->get_classes() as $object) { 47 | if (!$object->phpdocs || !count($object->phpdocs->get_tags('package', true))) { 48 | $errors[] = array('line' => $file->get_line_number($object->boundaries[0]), 49 | 'object' => 'class '. $object->name); 50 | } 51 | } 52 | foreach ($file->get_functions() as $object) { 53 | if ($object->class === false) { 54 | if (!$object->phpdocs || !count($object->phpdocs->get_tags('package', true))) { 55 | $errors[] = array('line' => $file->get_line_number($object->boundaries[0]), 56 | 'object' => 'function '. $object->fullname); 57 | } 58 | } 59 | } 60 | return $errors; 61 | } 62 | 63 | /** 64 | * Checks that wherever the package token is specified it is valid 65 | * 66 | * @param local_moodlecheck_file $file 67 | * @return array of found errors 68 | */ 69 | function local_moodlecheck_packagevalid(local_moodlecheck_file $file) { 70 | $errors = array(); 71 | $allowedpackages = local_moodlecheck_package_names($file); 72 | foreach ($file->get_all_phpdocs() as $phpdoc) { 73 | foreach ($phpdoc->get_tags('package') as $package) { 74 | if (!in_array($package, $allowedpackages)) { 75 | $errors[] = array('line' => $phpdoc->get_line_number($file, '@package'), 'package' => $package); 76 | } 77 | } 78 | } 79 | return $errors; 80 | } 81 | 82 | /** 83 | * Checks that wherever the category token is specified it is valid 84 | * 85 | * @param local_moodlecheck_file $file 86 | * @return array of found errors 87 | */ 88 | function local_moodlecheck_categoryvalid(local_moodlecheck_file $file) { 89 | $errors = array(); 90 | $allowedcategories = local_moodlecheck_get_categories($file); 91 | foreach ($file->get_all_phpdocs() as $phpdoc) { 92 | foreach ($phpdoc->get_tags('category') as $category) { 93 | if (!in_array($category, $allowedcategories)) { 94 | $errors[] = array('line' => $phpdoc->get_line_number($file, '@category'), 'category' => $category); 95 | } 96 | } 97 | } 98 | return $errors; 99 | } 100 | 101 | /** 102 | * Returns package names available for the file location 103 | * 104 | * If the file is inside plugin directory only frankenstyle name for this plugin is returned 105 | * Otherwise returns list of available core packages 106 | * 107 | * @param local_moodlecheck_file $file 108 | * @return array 109 | */ 110 | function local_moodlecheck_package_names(local_moodlecheck_file $file) { 111 | static $allplugins = array(); 112 | static $allsubsystems = array(); 113 | static $corepackages = array(); 114 | // Get and cache the list of plugins. 115 | if (empty($allplugins)) { 116 | $components = local_moodlecheck_path::get_components(); 117 | // First try to get the list from file components. 118 | if (isset($components['plugin'])) { 119 | $allplugins = $components['plugin']; 120 | } else { 121 | $allplugins = local_moodlecheck_get_plugins(); 122 | } 123 | } 124 | // Get and cache the list of subsystems. 125 | if (empty($allsubsystems)) { 126 | $components = local_moodlecheck_path::get_components(); 127 | // First try to get the list from file components. 128 | if (isset($components['subsystem'])) { 129 | $allsubsystems = $components['subsystem']; 130 | } else { 131 | $allsubsystems = get_core_subsystems(true); 132 | } 133 | // Prepare the list of core packages. 134 | foreach ($allsubsystems as $subsystem => $dir) { 135 | // Subsytems may come with the valid component name (core_ prefixed) already. 136 | if (strpos($subsystem, 'core_') === 0 or $subsystem === 'core') { 137 | $corepackages[] = $subsystem; 138 | } else { 139 | $corepackages[] = 'core_' . $subsystem; 140 | } 141 | } 142 | // Add "core" if missing. 143 | if (!in_array('core', $corepackages)) { 144 | $corepackages[] = 'core'; 145 | } 146 | } 147 | 148 | // Return valid plugin if the $file belongs to it. 149 | foreach ($allplugins as $pluginfullname => $dir) { 150 | if ($file->is_in_dir($dir)) { 151 | return array($pluginfullname); 152 | } 153 | } 154 | 155 | // If not return list of valid core packages. 156 | return $corepackages; 157 | } 158 | 159 | /** 160 | * Returns all installed plugins 161 | * 162 | * Returns all installed plugins as an associative array 163 | * with frankenstyle name as a key and plugin directory as a value 164 | * 165 | * @return array 166 | */ 167 | function &local_moodlecheck_get_plugins() { 168 | static $allplugins = array(); 169 | if (empty($allplugins)) { 170 | $plugintypes = get_plugin_types(); 171 | foreach ($plugintypes as $plugintype => $pluginbasedir) { 172 | if ($plugins = get_plugin_list($plugintype)) { 173 | foreach ($plugins as $plugin => $plugindir) { 174 | $allplugins[$plugintype.'_'.$plugin] = $plugindir; 175 | } 176 | } 177 | } 178 | asort($allplugins); 179 | $allplugins = array_reverse($allplugins, true); 180 | } 181 | return $allplugins; 182 | } 183 | 184 | /** 185 | * Reads the list of Core APIs from internet (or local copy) and returns the list of categories 186 | * 187 | * @param bool $forceoffline Disable fetching from the live docs site, useful for testing. 188 | * 189 | * @return array 190 | */ 191 | function &local_moodlecheck_get_categories($forceoffline = false) { 192 | global $CFG; 193 | static $allcategories = array(); 194 | if (empty($allcategories)) { 195 | $lastsavedtime = get_user_preferences('local_moodlecheck_categoriestime'); 196 | $lastsavedvalue = get_user_preferences('local_moodlecheck_categoriesvalue'); 197 | if ($lastsavedtime > time() - 24 * 60 * 60) { 198 | // Update only once per day. 199 | $allcategories = explode(',', $lastsavedvalue); 200 | } else { 201 | $allcategories = array(); 202 | $filecontent = false; 203 | if (!$forceoffline) { 204 | $filecontent = @file_get_contents("https://docs.moodle.org/dev/Core_APIs"); 205 | } 206 | if (empty($filecontent)) { 207 | $filecontent = file_get_contents($CFG->dirroot . '/local/moodlecheck/rules/coreapis.txt'); 208 | } 209 | preg_match_all('|.*API\s*\((.*)\)\s*|i', $filecontent, $matches); 210 | foreach ($matches[1] as $match) { 211 | $allcategories[] = trim(strip_tags(strtolower($match))); 212 | } 213 | set_user_preference('local_moodlecheck_categoriestime', time()); 214 | set_user_preference('local_moodlecheck_categoriesvalue', join(',', $allcategories)); 215 | } 216 | } 217 | return $allcategories; 218 | } 219 | -------------------------------------------------------------------------------- /locallib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Usefull classes for package local_moodlecheck 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | require_once($CFG->libdir . '/formslib.php'); 27 | require_once($CFG->dirroot. '/local/moodlecheck/file.php'); 28 | 29 | /** 30 | * Handles one rule 31 | * 32 | * @package local_moodlecheck 33 | * @copyright 2012 Marina Glancy 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class local_moodlecheck_rule { 37 | protected $code; 38 | protected $callback; 39 | protected $rulestring; 40 | protected $errorstring; 41 | protected $severity = 'error'; 42 | 43 | public function __construct($code) { 44 | $this->code = $code; 45 | } 46 | 47 | public function set_callback($callback) { 48 | $this->callback = $callback; 49 | return $this; 50 | } 51 | 52 | public function set_rulestring($rulestring) { 53 | $this->rulestring = $rulestring; 54 | return $this; 55 | } 56 | 57 | public function set_errorstring($errorstring) { 58 | $this->errorstring = $errorstring; 59 | return $this; 60 | } 61 | 62 | public function set_severity($severity) { 63 | $this->severity = $severity; 64 | return $this; 65 | } 66 | 67 | public function get_name() { 68 | if ($this->rulestring !== null && get_string_manager()->string_exists($this->rulestring, 'local_moodlecheck')) { 69 | return get_string($this->rulestring, 'local_moodlecheck'); 70 | } else if (get_string_manager()->string_exists('rule_'. $this->code, 'local_moodlecheck')) { 71 | return get_string('rule_'. $this->code, 'local_moodlecheck'); 72 | } else { 73 | return $this->code; 74 | } 75 | } 76 | 77 | public function get_error_message($args) { 78 | if (strlen($this->errorstring) && get_string_manager()->string_exists($this->errorstring, 'local_moodlecheck')) { 79 | return get_string($this->errorstring, 'local_moodlecheck', $args); 80 | } else if (get_string_manager()->string_exists('error_'. $this->code, 'local_moodlecheck')) { 81 | return get_string('error_'. $this->code, 'local_moodlecheck', $args); 82 | } else { 83 | if (isset($args['line'])) { 84 | // Do not dump line number, it will be included in the final message. 85 | unset($args['line']); 86 | } 87 | if (is_array($args)) { 88 | $args = ': '. var_export($args, true); 89 | } else if ($args !== true && $args !== null) { 90 | $args = ': '. $args; 91 | } else { 92 | $args = ''; 93 | } 94 | return $this->get_name(). '. Error'. $args; 95 | } 96 | } 97 | 98 | public function validatefile(local_moodlecheck_file $file) { 99 | $callback = $this->callback; 100 | $reterrors = $callback($file); 101 | $ruleerrors = array(); 102 | foreach ($reterrors as $args) { 103 | $ruleerrors[] = array( 104 | 'line' => $args['line'], 105 | 'severity' => $this->severity, 106 | 'message' => $this->get_error_message($args), 107 | 'source' => $this->code 108 | ); 109 | } 110 | return $ruleerrors; 111 | } 112 | } 113 | 114 | /** 115 | * Rule registry 116 | * 117 | * @package local_moodlecheck 118 | * @copyright 2012 Marina Glancy 119 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 120 | */ 121 | class local_moodlecheck_registry { 122 | protected static $rules = array(); 123 | protected static $enabledrules = array(); 124 | 125 | public static function add_rule($code) { 126 | $rule = new local_moodlecheck_rule($code); 127 | self::$rules[$code] = $rule; 128 | return $rule; 129 | } 130 | 131 | public static function get_registered_rules() { 132 | return self::$rules; 133 | } 134 | 135 | public static function enable_rule($code, $enable = true) { 136 | if (!isset(self::$rules[$code])) { 137 | // Can not enable/disable unexisting rule. 138 | return; 139 | } 140 | if (!$enable) { 141 | if (isset(self::$enabledrules[$code])) { 142 | unset(self::$enabledrules[$code]); 143 | } 144 | } else { 145 | self::$enabledrules[$code] = self::$rules[$code]; 146 | } 147 | } 148 | 149 | public static function &get_enabled_rules() { 150 | return self::$enabledrules; 151 | } 152 | 153 | public static function enable_all_rules() { 154 | foreach (array_keys(self::$rules) as $code) { 155 | self::enable_rule($code); 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * Handles one path being validated (file or directory) 162 | * 163 | * @package local_moodlecheck 164 | * @copyright 2012 Marina Glancy 165 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 166 | */ 167 | class local_moodlecheck_path { 168 | protected $path = null; 169 | protected $ignorepaths = null; 170 | protected $file = null; 171 | protected $subpaths = null; 172 | protected $validated = false; 173 | protected $rootpath = true; 174 | 175 | public function __construct($path, $ignorepaths) { 176 | $path = clean_param(trim($path), PARAM_PATH); 177 | // If the path is already one existing full path 178 | // accept it, else assume it's a relative one. 179 | if (!file_exists($path) and substr($path, 0, 1) == '/') { 180 | $path = substr($path, 1); 181 | } 182 | $this->path = $path; 183 | $this->ignorepaths = $ignorepaths; 184 | } 185 | 186 | public function get_fullpath() { 187 | global $CFG; 188 | // It's already one full path. 189 | if (file_exists($this->path)) { 190 | return $this->path; 191 | } 192 | return $CFG->dirroot. '/'. $this->path; 193 | } 194 | 195 | public function validate() { 196 | if ($this->validated) { 197 | // Prevent from second validation. 198 | return; 199 | } 200 | if (is_file($this->get_fullpath())) { 201 | $this->file = new local_moodlecheck_file($this->get_fullpath()); 202 | } else if (is_dir($this->get_fullpath())) { 203 | $this->subpaths = array(); 204 | if ($dh = opendir($this->get_fullpath())) { 205 | while (($file = readdir($dh)) !== false) { 206 | if ($file != '.' && $file != '..' && $file != '.git' && $file != '.hg' && !$this->is_ignored($file)) { 207 | $subpath = new local_moodlecheck_path($this->path . '/'. $file, $this->ignorepaths); 208 | $subpath->set_rootpath(false); 209 | $this->subpaths[] = $subpath; 210 | } 211 | } 212 | closedir($dh); 213 | } 214 | } 215 | $this->validated = true; 216 | } 217 | 218 | protected function is_ignored($file) { 219 | $filepath = $this->path. '/'. $file; 220 | foreach ($this->ignorepaths as $ignorepath) { 221 | $ignorepath = rtrim($ignorepath, '/'); 222 | if ($filepath == $ignorepath || substr($filepath, 0, strlen($ignorepath) + 1) == $ignorepath . '/') { 223 | return true; 224 | } 225 | } 226 | return false; 227 | } 228 | 229 | public function is_file() { 230 | return $this->file !== null; 231 | } 232 | 233 | public function is_dir() { 234 | return $this->subpaths !== null; 235 | } 236 | 237 | public function get_path() { 238 | return $this->path; 239 | } 240 | 241 | public function get_file() { 242 | return $this->file; 243 | } 244 | 245 | public function get_subpaths() { 246 | return $this->subpaths; 247 | } 248 | 249 | protected function set_rootpath($rootpath) { 250 | $this->rootpath = (boolean)$rootpath; 251 | } 252 | 253 | public function is_rootpath() { 254 | return $this->rootpath; 255 | } 256 | 257 | public static function get_components($componentsfile = null) { 258 | static $components = array(); 259 | if (!empty($components)) { 260 | return $components; 261 | } 262 | if (empty($componentsfile)) { 263 | return array(); 264 | } 265 | if (file_exists($componentsfile) and is_readable($componentsfile)) { 266 | $fh = fopen($componentsfile, 'r'); 267 | while (($line = fgets($fh, 4096)) !== false) { 268 | $split = explode(',', $line); 269 | if (count($split) != 3) { 270 | // Wrong count of elements in the line. 271 | continue; 272 | } 273 | if (trim($split[0]) != 'plugin' and trim($split[0]) != 'subsystem') { 274 | // Wrong type. 275 | continue; 276 | } 277 | // Let's assume it's a correct line. 278 | $components[trim($split[0])][trim($split[1])] = trim($split[2]); 279 | } 280 | fclose($fh); 281 | } 282 | return $components; 283 | } 284 | } 285 | 286 | /** 287 | * Form for check options 288 | * 289 | * @package local_moodlecheck 290 | * @copyright 2012 Marina Glancy 291 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 292 | */ 293 | class local_moodlecheck_form extends moodleform { 294 | protected function definition() { 295 | $mform = $this->_form; 296 | 297 | $mform->addElement('textarea', 'path', get_string('path', 'local_moodlecheck'), 298 | array('rows' => 8, 'cols' => 120)); 299 | $mform->addHelpButton('path', 'path', 'local_moodlecheck'); 300 | 301 | $mform->addElement('header', 'selectivecheck', get_string('options')); 302 | $mform->setExpanded('selectivecheck', false); 303 | 304 | $mform->addElement('textarea', 'ignorepath', get_string('ignorepath', 'local_moodlecheck'), 305 | array('rows' => 3, 'cols' => 120)); 306 | 307 | $mform->addElement('radio', 'checkall', '', get_string('checkallrules', 'local_moodlecheck'), 'all'); 308 | $mform->addElement('radio', 'checkall', '', get_string('checkselectedrules', 'local_moodlecheck'), 'selected'); 309 | $mform->setDefault('checkall', 'all'); 310 | 311 | $group = array(); 312 | foreach (local_moodlecheck_registry::get_registered_rules() as $code => $rule) { 313 | $group[] =& $mform->createElement('checkbox', "rule[$code]", ' ', $rule->get_name()); 314 | } 315 | $mform->addGroup($group, 'checkboxgroup', '', array('
'), false); 316 | foreach (local_moodlecheck_registry::get_registered_rules() as $code => $rule) { 317 | $group[] =& $mform->createElement('checkbox', "rule[$code]", ' ', $rule->get_name()); 318 | $mform->setDefault("rule[$code]", 1); 319 | $mform->disabledIf("rule[$code]", 'checkall', 'eq', 'all'); 320 | } 321 | 322 | $this->add_action_buttons(false, get_string('check', 'local_moodlecheck')); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /tests/moodlecheck_rules_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * This file contains unit tests for covering "moodle" PHPDoc rules. 19 | * 20 | * @package local_moodlecheck 21 | * @subpackage phpunit 22 | * @category phpunit 23 | * @copyright 2018 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | 27 | defined('MOODLE_INTERNAL') || die(); // Remove this to use me out from Moodle. 28 | 29 | class local_moodlecheck_rules_testcase extends advanced_testcase { 30 | 31 | public function setUp(): void { 32 | global $CFG; 33 | parent::setUp(); 34 | // Add the moodlecheck machinery. 35 | require_once($CFG->dirroot . '/local/moodlecheck/locallib.php'); 36 | // Load all files from rules directory. 37 | if ($dh = opendir($CFG->dirroot. '/local/moodlecheck/rules')) { 38 | while (($file = readdir($dh)) !== false) { 39 | if ($file != '.' && $file != '..') { 40 | $pathinfo = pathinfo($file); 41 | if (isset($pathinfo['extension']) && $pathinfo['extension'] == 'php') { 42 | require_once($CFG->dirroot. '/local/moodlecheck/rules/'. $file); 43 | } 44 | } 45 | } 46 | closedir($dh); 47 | } 48 | // Load all rules. 49 | local_moodlecheck_registry::enable_all_rules(); 50 | } 51 | 52 | /** 53 | * Verify the ::class constant is not reported as phpdoc problem. 54 | */ 55 | public function test_constantclass() { 56 | global $PAGE; 57 | $output = $PAGE->get_renderer('local_moodlecheck'); 58 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/constantclass.php ', null); 59 | $result = $output->display_path($path, 'xml'); 60 | 61 | // Convert results to XML Objext. 62 | $xmlresult = new DOMDocument(); 63 | $xmlresult->loadXML($result); 64 | 65 | // Let's verify we have received a xml with file top element and 2 children. 66 | $xpath = new DOMXpath($xmlresult); 67 | $found = $xpath->query("//file/error"); 68 | // TODO: Change to DOMNodeList::count() when php71 support is gone. 69 | $this->assertSame(2, $found->length); 70 | 71 | // Also verify that contents do not include any problem with line 42 / classesdocumented. Use simple string matching here. 72 | $this->assertStringContainsString('line="20"', $result); 73 | $this->assertStringContainsString('packagevalid', $result); 74 | $this->assertStringNotContainsString('line="42"', $result); 75 | $this->assertStringNotContainsString('classesdocumented', $result); 76 | } 77 | 78 | /** 79 | * Assert that the file block is required for old files, and not for 1-artifact ones. 80 | */ 81 | public function test_file_block_required() { 82 | global $PAGE; 83 | 84 | $output = $PAGE->get_renderer('local_moodlecheck'); 85 | 86 | // A file with multiple classes, require the file phpdoc block. 87 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_file_required_yes1.php', null); 88 | $result = $output->display_path($path, 'xml'); 89 | $this->assertStringContainsString('File-level phpdocs block is not found', $result); 90 | 91 | // A file without any class (library-like), require the file phpdoc block. 92 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_file_required_yes2.php', null); 93 | $result = $output->display_path($path, 'xml'); 94 | $this->assertStringContainsString('File-level phpdocs block is not found', $result); 95 | 96 | // A file with one interface and one trait, require the file phpdoc block. 97 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_file_required_yes3.php', null); 98 | $result = $output->display_path($path, 'xml'); 99 | $this->assertStringContainsString('File-level phpdocs block is not found', $result); 100 | 101 | // A file with only one class, do not require the file phpdoc block. 102 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_file_required_no1.php', null); 103 | $result = $output->display_path($path, 'xml'); 104 | $this->assertStringNotContainsString('File-level phpdocs block is not found', $result); 105 | 106 | // A file with only one interface, do not require the file phpdoc block. 107 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_file_required_no2.php', null); 108 | $result = $output->display_path($path, 'xml'); 109 | $this->assertStringNotContainsString('File-level phpdocs block is not found', $result); 110 | 111 | // A file with only one trait, do not require the file phpdoc block. 112 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_file_required_no3.php', null); 113 | $result = $output->display_path($path, 'xml'); 114 | $this->assertStringNotContainsString('File-level phpdocs block is not found', $result); 115 | } 116 | 117 | /** 118 | * Assert that classes do not need to have any particular phpdocs tags. 119 | */ 120 | public function test_classtags() { 121 | global $PAGE; 122 | 123 | $output = $PAGE->get_renderer('local_moodlecheck'); 124 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/classtags.php ', null); 125 | 126 | $result = $output->display_path($path, 'xml'); 127 | 128 | $this->assertStringNotContainsString('classeshavecopyright', $result); 129 | $this->assertStringNotContainsString('classeshavelicense', $result); 130 | } 131 | 132 | /** 133 | * Verify various phpdoc tags in general directories. 134 | */ 135 | public function test_phpdoc_tags_general() { 136 | global $PAGE; 137 | $output = $PAGE->get_renderer('local_moodlecheck'); 138 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_tags_general.php ', null); 139 | $result = $output->display_path($path, 'xml'); 140 | 141 | // Convert results to XML Objext. 142 | $xmlresult = new DOMDocument(); 143 | $xmlresult->loadXML($result); 144 | 145 | // Let's verify we have received a xml with file top element and 8 children. 146 | $xpath = new DOMXpath($xmlresult); 147 | $found = $xpath->query("//file/error"); 148 | // TODO: Change to DOMNodeList::count() when php71 support is gone. 149 | $this->assertSame(19, $found->length); 150 | 151 | // Also verify various bits by content. 152 | $this->assertStringContainsString('packagevalid', $result); 153 | $this->assertStringContainsString('incomplete_param_annotation has incomplete parameters list', $result); 154 | $this->assertStringContainsString('missing_param_defintion has incomplete parameters list', $result); 155 | $this->assertStringContainsString('missing_param_annotation has incomplete parameters list', $result); 156 | $this->assertStringContainsString('incomplete_param_definition has incomplete parameters list', $result); 157 | $this->assertStringContainsString('incomplete_param_annotation1 has incomplete parameters list', $result); 158 | $this->assertStringContainsString('mismatch_param_types has incomplete parameters list', $result); 159 | $this->assertStringContainsString('mismatch_param_types1 has incomplete parameters list', $result); 160 | $this->assertStringContainsString('mismatch_param_types2 has incomplete parameters list', $result); 161 | $this->assertStringContainsString('mismatch_param_types3 has incomplete parameters list', $result); 162 | $this->assertStringContainsString('incomplete_return_annotation has incomplete parameters list', $result); 163 | $this->assertStringContainsString('Invalid phpdocs tag @small', $result); 164 | $this->assertStringContainsString('Invalid phpdocs tag @zzzing', $result); 165 | $this->assertStringContainsString('Invalid phpdocs tag @inheritdoc', $result); 166 | $this->assertStringContainsString('Incorrect path for phpdocs tag @covers', $result); 167 | $this->assertStringContainsString('Incorrect path for phpdocs tag @dataProvider', $result); 168 | $this->assertStringContainsString('Incorrect path for phpdocs tag @group', $result); 169 | $this->assertStringContainsString('@codingStandardsIgnoreLine', $result); 170 | $this->assertStringNotContainsString('@deprecated', $result); 171 | $this->assertStringNotContainsString('correct_param_types', $result); 172 | $this->assertStringNotContainsString('correct_return_type', $result); 173 | } 174 | 175 | /** 176 | * Verify various phpdoc tags in tests directories. 177 | */ 178 | public function test_phpdoc_tags_tests() { 179 | global $PAGE; 180 | $output = $PAGE->get_renderer('local_moodlecheck'); 181 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_tags_test.php ', null); 182 | $result = $output->display_path($path, 'xml'); 183 | 184 | // Convert results to XML Objext. 185 | $xmlresult = new DOMDocument(); 186 | $xmlresult->loadXML($result); 187 | 188 | // Let's verify we have received a xml with file top element and 5 children. 189 | $xpath = new DOMXpath($xmlresult); 190 | $found = $xpath->query("//file/error"); 191 | // TODO: Change to DOMNodeList::count() when php71 support is gone. 192 | $this->assertSame(5, $found->length); 193 | 194 | // Also verify various bits by content. 195 | $this->assertStringContainsString('packagevalid', $result); 196 | $this->assertStringContainsString('Invalid phpdocs tag @small', $result); 197 | $this->assertStringContainsString('Invalid phpdocs tag @zzzing', $result); 198 | $this->assertStringContainsString('Invalid phpdocs tag @inheritdoc', $result); 199 | $this->assertStringNotContainsString('Incorrect path for phpdocs tag @covers', $result); 200 | $this->assertStringNotContainsString('Incorrect path for phpdocs tag @dataProvider', $result); 201 | $this->assertStringNotContainsString('Incorrect path for phpdocs tag @group', $result); 202 | $this->assertStringNotContainsString('@deprecated', $result); 203 | } 204 | 205 | /** 206 | * Verify various phpdoc tags can be used inline. 207 | */ 208 | public function test_phpdoc_tags_inline() { 209 | global $PAGE; 210 | $output = $PAGE->get_renderer('local_moodlecheck'); 211 | $path = new local_moodlecheck_path('local/moodlecheck/tests/fixtures/phpdoc_tags_inline.php ', null); 212 | $result = $output->display_path($path, 'xml'); 213 | 214 | // Convert results to XML Objext. 215 | $xmlresult = new DOMDocument(); 216 | $xmlresult->loadXML($result); 217 | 218 | // Let's verify we have received a xml with file top element and 8 children. 219 | $xpath = new DOMXpath($xmlresult); 220 | $found = $xpath->query("//file/error"); 221 | // TODO: Change to DOMNodeList::count() when php71 support is gone. 222 | $this->assertSame(8, $found->length); 223 | 224 | // Also verify various bits by content. 225 | $this->assertStringContainsString('packagevalid', $result); 226 | $this->assertStringContainsString('Invalid inline phpdocs tag @param found', $result); 227 | $this->assertStringContainsString('Invalid inline phpdocs tag @throws found', $result); 228 | $this->assertStringContainsString('Inline phpdocs tag {@link tags have to be 1 url} with incorrect', $result); 229 | $this->assertStringContainsString('Inline phpdocs tag {@see must be 1 word only} with incorrect', $result); 230 | $this->assertStringContainsString('Inline phpdocs tag {@see $this->tagrules['url']} with incorrect', $result); 231 | $this->assertStringContainsString('Inline phpdocs tag not enclosed with curly brackets @see found', $result); 232 | $this->assertStringContainsString('It must match {@link valid URL} or {@see valid FQSEN}', $result); 233 | $this->assertStringNotContainsString('{@link https://moodle.org}', $result); 234 | $this->assertStringNotContainsString('{@see has_capability}', $result); 235 | $this->assertStringNotContainsString('ba8by}', $result); 236 | } 237 | 238 | /** 239 | * Test that {@see local_moodlecheck_get_categories()} returns the correct list of allowed categories. 240 | */ 241 | public function test_local_moodlecheck_get_categories() { 242 | 243 | set_user_preference('local_moodlecheck_categoriestime', 0); 244 | set_user_preference('local_moodlecheck_categoriesvalue', ''); 245 | 246 | $allowed = local_moodlecheck_get_categories(); 247 | 248 | $expected = ['access', 'dml', 'files', 'form', 'log', 'navigation', 'page', 'output', 'string', 'upgrade', 249 | 'core', 'admin', 'analytics', 'availability', 'backup', 'cache', 'calendar', 'check', 'comment', 250 | 'competency', 'ddl', 'enrol', 'event', 'xapi', 'external', 'lock', 'message', 'media', 'oauth2', 251 | 'preference', 'portfolio', 'privacy', 'rating', 'rss', 'search', 'tag', 'task', 'time', 'test', 252 | 'webservice', 'badges', 'completion', 'grading', 'group', 'grade', 'plagiarism', 'question', 253 | ]; 254 | 255 | foreach ($expected as $category) { 256 | $this->assertContains($category, $allowed); 257 | } 258 | 259 | // Also check that the locally cached copy is still up to date. 260 | $allowed = local_moodlecheck_get_categories(true); 261 | 262 | foreach ($expected as $category) { 263 | $this->assertContains($category, $allowed); 264 | } 265 | } 266 | 267 | /** 268 | * Verify that anonymous classes do not require phpdoc class blocks. 269 | * 270 | * @dataProvider anonymous_class_provider 271 | * @param string $path 272 | * @param bool $expectclassesdocumentedfail Whether the 273 | */ 274 | public function test_phpdoc_anonymous_class_docblock(string $path, bool $expectclassesdocumentedfail) { 275 | global $PAGE; 276 | 277 | $output = $PAGE->get_renderer('local_moodlecheck'); 278 | $checkpath = new local_moodlecheck_path($path, null); 279 | $result = $output->display_path($checkpath, 'xml'); 280 | 281 | if ($expectclassesdocumentedfail) { 282 | $this->assertStringContainsString('classesdocumented', $result); 283 | } else { 284 | $this->assertStringNotContainsString('classesdocumented', $result); 285 | } 286 | } 287 | 288 | /** 289 | * Data provider for anonymous classes tests. 290 | * 291 | * @return array 292 | */ 293 | public function anonymous_class_provider(): array { 294 | $rootpath = 'local/moodlecheck/tests/fixtures/anonymous'; 295 | return [ 296 | 'return new class {' => [ 297 | "{$rootpath}/anonymous.php", 298 | false, 299 | ], 300 | 'return new class extends parentclass {' => [ 301 | "{$rootpath}/extends.php", 302 | false, 303 | ], 304 | 'return new class implements someinterface {' => [ 305 | "{$rootpath}/implements.php", 306 | false, 307 | ], 308 | 'return new class extends parentclass implements someinterface {' => [ 309 | "{$rootpath}/extendsandimplements.php", 310 | false, 311 | ], 312 | '$value = new class {' => [ 313 | "{$rootpath}/assigned.php", 314 | false, 315 | ], 316 | 'class someclass extends parentclass {' => [ 317 | "{$rootpath}/named.php", 318 | true, 319 | ], 320 | ]; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /rules/phpdocs_basic.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Registering rules for phpdocs checking 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | local_moodlecheck_registry::add_rule('noemptysecondline')->set_callback('local_moodlecheck_noemptysecondline') 28 | ->set_severity('warning'); 29 | local_moodlecheck_registry::add_rule('filephpdocpresent')->set_callback('local_moodlecheck_filephpdocpresent'); 30 | local_moodlecheck_registry::add_rule('classesdocumented')->set_callback('local_moodlecheck_classesdocumented'); 31 | local_moodlecheck_registry::add_rule('functionsdocumented')->set_callback('local_moodlecheck_functionsdocumented'); 32 | local_moodlecheck_registry::add_rule('variablesdocumented')->set_callback('local_moodlecheck_variablesdocumented'); 33 | local_moodlecheck_registry::add_rule('constsdocumented')->set_callback('local_moodlecheck_constsdocumented'); 34 | local_moodlecheck_registry::add_rule('definesdocumented')->set_callback('local_moodlecheck_definesdocumented'); 35 | local_moodlecheck_registry::add_rule('noinlinephpdocs')->set_callback('local_moodlecheck_noinlinephpdocs'); 36 | local_moodlecheck_registry::add_rule('phpdocsfistline')->set_callback('local_moodlecheck_phpdocsfistline'); 37 | local_moodlecheck_registry::add_rule('functiondescription')->set_callback('local_moodlecheck_functiondescription'); 38 | local_moodlecheck_registry::add_rule('functionarguments')->set_callback('local_moodlecheck_functionarguments'); 39 | local_moodlecheck_registry::add_rule('variableshasvar')->set_callback('local_moodlecheck_variableshasvar'); 40 | local_moodlecheck_registry::add_rule('definedoccorrect')->set_callback('local_moodlecheck_definedoccorrect'); 41 | local_moodlecheck_registry::add_rule('filehascopyright')->set_callback('local_moodlecheck_filehascopyright'); 42 | local_moodlecheck_registry::add_rule('filehaslicense')->set_callback('local_moodlecheck_filehaslicense'); 43 | local_moodlecheck_registry::add_rule('phpdocsinvalidtag')->set_callback('local_moodlecheck_phpdocsinvalidtag'); 44 | local_moodlecheck_registry::add_rule('phpdocsnotrecommendedtag')->set_callback('local_moodlecheck_phpdocsnotrecommendedtag') 45 | ->set_severity('warning'); 46 | local_moodlecheck_registry::add_rule('phpdocsinvalidpathtag')->set_callback('local_moodlecheck_phpdocsinvalidpathtag') 47 | ->set_severity('warning'); 48 | local_moodlecheck_registry::add_rule('phpdocsinvalidinlinetag')->set_callback('local_moodlecheck_phpdocsinvalidinlinetag'); 49 | local_moodlecheck_registry::add_rule('phpdocsuncurlyinlinetag')->set_callback('local_moodlecheck_phpdocsuncurlyinlinetag'); 50 | local_moodlecheck_registry::add_rule('phpdoccontentsinlinetag')->set_callback('local_moodlecheck_phpdoccontentsinlinetag'); 51 | 52 | /** 53 | * Checks if the first line in the file has open tag and second line is not empty 54 | * 55 | * @param local_moodlecheck_file $file 56 | * @return array of found errors 57 | */ 58 | function local_moodlecheck_noemptysecondline(local_moodlecheck_file $file) { 59 | $tokens = &$file->get_tokens(); 60 | if ($tokens[0][0] == T_OPEN_TAG && !$file->is_whitespace_token(1) && $file->is_multiline_token(0) == 1) { 61 | return array(); 62 | } 63 | return array(array('line' => 2)); 64 | } 65 | 66 | /** 67 | * Checks if file-level phpdocs block is present 68 | * 69 | * @param local_moodlecheck_file $file 70 | * @return array of found errors 71 | */ 72 | function local_moodlecheck_filephpdocpresent(local_moodlecheck_file $file) { 73 | // This rule doesn't apply if the file is 1-artifact file (see #66). 74 | $artifacts = $file->get_artifacts(); 75 | if (count($artifacts[T_CLASS]) + count($artifacts[T_INTERFACE]) + count($artifacts[T_TRAIT]) === 1) { 76 | return array(); 77 | } 78 | if ($file->find_file_phpdocs() === false) { 79 | $tokens = &$file->get_tokens(); 80 | for ($i = 0; $i < 30; $i++) { 81 | if (isset($tokens[$i]) && !in_array($tokens[$i][0], array(T_OPEN_TAG, T_WHITESPACE, T_COMMENT))) { 82 | return array(array('line' => $file->get_line_number($i))); 83 | } 84 | } 85 | // For some reason we cound not find the line number. 86 | return array(array('line' => '')); 87 | } 88 | return array(); 89 | } 90 | 91 | /** 92 | * Checks if all classes have phpdocs blocks 93 | * 94 | * @param local_moodlecheck_file $file 95 | * @return array of found errors 96 | */ 97 | function local_moodlecheck_classesdocumented(local_moodlecheck_file $file) { 98 | $errors = array(); 99 | foreach ($file->get_classes() as $class) { 100 | if ($class->phpdocs === false) { 101 | $errors[] = array('class' => $class->name, 'line' => $file->get_line_number($class->boundaries[0])); 102 | } 103 | } 104 | return $errors; 105 | } 106 | 107 | /** 108 | * Checks if all functions have phpdocs blocks 109 | * 110 | * @param local_moodlecheck_file $file 111 | * @return array of found errors 112 | */ 113 | function local_moodlecheck_functionsdocumented(local_moodlecheck_file $file) { 114 | 115 | $isphpunitfile = preg_match('#/tests/[^/]+_test\.php$#', $file->get_filepath()); 116 | $errors = array(); 117 | foreach ($file->get_functions() as $function) { 118 | if ($function->phpdocs === false) { 119 | // Exception is made for plain phpunit test methods MDLSITE-3282, MDLSITE-3856. 120 | $istestmethod = (strpos($function->name, 'test_') === 0 or 121 | stripos($function->name, 'setup') === 0 or 122 | stripos($function->name, 'teardown') === 0); 123 | if (!($isphpunitfile && $istestmethod)) { 124 | $errors[] = array('function' => $function->fullname, 'line' => $file->get_line_number($function->boundaries[0])); 125 | } 126 | } 127 | } 128 | return $errors; 129 | } 130 | 131 | /** 132 | * Checks if all variables have phpdocs blocks 133 | * 134 | * @param local_moodlecheck_file $file 135 | * @return array of found errors 136 | */ 137 | function local_moodlecheck_variablesdocumented(local_moodlecheck_file $file) { 138 | $errors = array(); 139 | foreach ($file->get_variables() as $variable) { 140 | if ($variable->phpdocs === false) { 141 | $errors[] = array('variable' => $variable->fullname, 'line' => $file->get_line_number($variable->tid)); 142 | } 143 | } 144 | return $errors; 145 | } 146 | 147 | /** 148 | * Checks if all constants have phpdocs blocks 149 | * 150 | * @param local_moodlecheck_file $file 151 | * @return array of found errors 152 | */ 153 | function local_moodlecheck_constsdocumented(local_moodlecheck_file $file) { 154 | $errors = array(); 155 | foreach ($file->get_constants() as $object) { 156 | if ($object->phpdocs === false) { 157 | $errors[] = array('object' => $object->fullname, 'line' => $file->get_line_number($object->tid)); 158 | } 159 | } 160 | return $errors; 161 | } 162 | 163 | /** 164 | * Checks if all variables have phpdocs blocks 165 | * 166 | * @param local_moodlecheck_file $file 167 | * @return array of found errors 168 | */ 169 | function local_moodlecheck_definesdocumented(local_moodlecheck_file $file) { 170 | $errors = array(); 171 | foreach ($file->get_defines() as $object) { 172 | if ($object->phpdocs === false) { 173 | $errors[] = array('object' => $object->fullname, 'line' => $file->get_line_number($object->tid)); 174 | } 175 | } 176 | return $errors; 177 | } 178 | 179 | /** 180 | * Checks that no comment starts with three or more slashes 181 | * 182 | * @param local_moodlecheck_file $file 183 | * @return array of found errors 184 | */ 185 | function local_moodlecheck_noinlinephpdocs(local_moodlecheck_file $file) { 186 | $errors = array(); 187 | foreach ($file->get_all_phpdocs() as $phpdocs) { 188 | if ($phpdocs->is_inline()) { 189 | $errors[] = array('line' => $phpdocs->get_line_number($file)); 190 | } 191 | } 192 | return $errors; 193 | } 194 | 195 | /** 196 | * Check that all the phpdoc tags used are valid ones 197 | * 198 | * @param local_moodlecheck_file $file 199 | * @return array of found errors 200 | */ 201 | function local_moodlecheck_phpdocsinvalidtag(local_moodlecheck_file $file) { 202 | $errors = array(); 203 | foreach ($file->get_all_phpdocs() as $phpdocs) { 204 | foreach ($phpdocs->get_tags() as $tag) { 205 | $tag = preg_replace('|^@([^\s]*).*|s', '$1', $tag); 206 | if (!in_array($tag, local_moodlecheck_phpdocs::$validtags)) { 207 | $errors[] = array( 208 | 'line' => $phpdocs->get_line_number($file, '@' . $tag), 209 | 'tag' => '@' . $tag); 210 | } 211 | } 212 | } 213 | return $errors; 214 | } 215 | 216 | /** 217 | * Check that all the phpdoc tags used are recommended ones 218 | * 219 | * @param local_moodlecheck_file $file 220 | * @return array of found errors 221 | */ 222 | function local_moodlecheck_phpdocsnotrecommendedtag(local_moodlecheck_file $file) { 223 | $errors = array(); 224 | foreach ($file->get_all_phpdocs() as $phpdocs) { 225 | foreach ($phpdocs->get_tags() as $tag) { 226 | $tag = preg_replace('|^@([^\s]*).*|s', '$1', $tag); 227 | if (in_array($tag, local_moodlecheck_phpdocs::$validtags) and 228 | !in_array($tag, local_moodlecheck_phpdocs::$recommendedtags)) { 229 | $errors[] = array( 230 | 'line' => $phpdocs->get_line_number($file, '@' . $tag), 231 | 'tag' => '@' . $tag); 232 | } 233 | } 234 | } 235 | return $errors; 236 | } 237 | 238 | /** 239 | * Check that all the path-restricted phpdoc tags used are in place 240 | * 241 | * @param local_moodlecheck_file $file 242 | * @return array of found errors 243 | */ 244 | function local_moodlecheck_phpdocsinvalidpathtag(local_moodlecheck_file $file) { 245 | $errors = array(); 246 | foreach ($file->get_all_phpdocs() as $phpdocs) { 247 | foreach ($phpdocs->get_tags() as $tag) { 248 | $tag = preg_replace('|^@([^\s]*).*|s', '$1', $tag); 249 | if (in_array($tag, local_moodlecheck_phpdocs::$validtags) and 250 | in_array($tag, local_moodlecheck_phpdocs::$recommendedtags) and 251 | isset(local_moodlecheck_phpdocs::$pathrestrictedtags[$tag])) { 252 | // Verify file path matches some of the valid paths for the tag. 253 | if (!preg_filter(local_moodlecheck_phpdocs::$pathrestrictedtags[$tag], '$0', $file->get_filepath())) { 254 | $errors[] = array( 255 | 'line' => $phpdocs->get_line_number($file, '@' . $tag), 256 | 'tag' => '@' . $tag); 257 | } 258 | } 259 | } 260 | } 261 | return $errors; 262 | } 263 | 264 | /** 265 | * Check that all the inline phpdoc tags found are valid 266 | * 267 | * @param local_moodlecheck_file $file 268 | * @return array of found errors 269 | */ 270 | function local_moodlecheck_phpdocsinvalidinlinetag(local_moodlecheck_file $file) { 271 | $errors = array(); 272 | foreach ($file->get_all_phpdocs() as $phpdocs) { 273 | if ($inlinetags = $phpdocs->get_inline_tags(false)) { 274 | foreach ($inlinetags as $inlinetag) { 275 | if (!in_array($inlinetag, local_moodlecheck_phpdocs::$inlinetags)) { 276 | $errors[] = array( 277 | 'line' => $phpdocs->get_line_number($file, '@' . $inlinetag), 278 | 'tag' => '@' . $inlinetag); 279 | } 280 | } 281 | } 282 | } 283 | return $errors; 284 | } 285 | 286 | /** 287 | * Check that all the valid inline tags are properly enclosed with curly brackets 288 | * @param local_moodlecheck_file $file 289 | * @return array of found errors 290 | */ 291 | function local_moodlecheck_phpdocsuncurlyinlinetag(local_moodlecheck_file $file) { 292 | $errors = array(); 293 | foreach ($file->get_all_phpdocs() as $phpdocs) { 294 | if ($inlinetags = $phpdocs->get_inline_tags(false)) { 295 | $curlyinlinetags = $phpdocs->get_inline_tags(true); 296 | // The difference will tell us which ones are nor enclosed by curly brackets. 297 | foreach ($curlyinlinetags as $remove) { 298 | foreach ($inlinetags as $k => $v) { 299 | if ($v === $remove) { 300 | unset($inlinetags[$k]); 301 | break; 302 | } 303 | } 304 | } 305 | foreach ($inlinetags as $inlinetag) { 306 | if (in_array($inlinetag, local_moodlecheck_phpdocs::$inlinetags)) { 307 | $errors[] = array( 308 | 'line' => $phpdocs->get_line_number($file, ' @' . $inlinetag), 309 | 'tag' => '@' . $inlinetag); 310 | } 311 | } 312 | } 313 | } 314 | return $errors; 315 | } 316 | 317 | /** 318 | * Check that all the valid inline curly tags have correct contents. 319 | * 320 | * @param local_moodlecheck_file $file 321 | * @return array of found errors 322 | */ 323 | function local_moodlecheck_phpdoccontentsinlinetag(local_moodlecheck_file $file) { 324 | $errors = array(); 325 | foreach ($file->get_all_phpdocs() as $phpdocs) { 326 | if ($curlyinlinetags = $phpdocs->get_inline_tags(true, true)) { 327 | foreach ($curlyinlinetags as $curlyinlinetag) { 328 | // Split into tag and content. 329 | list($tag, $content) = explode(' ', $curlyinlinetag, 2); 330 | if (in_array($tag, local_moodlecheck_phpdocs::$inlinetags)) { 331 | switch ($tag) { 332 | case 'link': // Must be a correct URL. 333 | if (!filter_var($content, FILTER_VALIDATE_URL)) { 334 | $errors[] = array( 335 | 'line' => $phpdocs->get_line_number($file, ' {@' . $curlyinlinetag), 336 | 'tag' => '{@' . $curlyinlinetag . '}'); 337 | } 338 | break; 339 | case 'see': // Must be 1-word (with some chars allowed - FQSEN only. 340 | if (str_word_count($content, 0, '\()-_:>$012345789') !== 1) { 341 | $errors[] = array( 342 | 'line' => $phpdocs->get_line_number($file, ' {@' . $curlyinlinetag), 343 | 'tag' => '{@' . $curlyinlinetag . '}'); 344 | } 345 | break; 346 | } 347 | } 348 | } 349 | } 350 | } 351 | return $errors; 352 | } 353 | 354 | /** 355 | * Makes sure that file-level phpdocs and all classes have one-line short description 356 | * 357 | * @param local_moodlecheck_file $file 358 | * @return array of found errors 359 | */ 360 | function local_moodlecheck_phpdocsfistline(local_moodlecheck_file $file) { 361 | $errors = array(); 362 | 363 | if (($phpdocs = $file->find_file_phpdocs()) && !$file->find_file_phpdocs()->get_shortdescription()) { 364 | $errors[] = array( 365 | 'line' => $phpdocs->get_line_number($file), 366 | 'object' => 'file' 367 | ); 368 | } 369 | foreach ($file->get_classes() as $class) { 370 | if ($class->phpdocs && !$class->phpdocs->get_shortdescription()) { 371 | $errors[] = array( 372 | 'line' => $class->phpdocs->get_line_number($file), 373 | 'object' => 'class '.$class->name 374 | ); 375 | } 376 | } 377 | return $errors; 378 | } 379 | 380 | /** 381 | * Makes sure that all functions have descriptions 382 | * 383 | * @param local_moodlecheck_file $file 384 | * @return array of found errors 385 | */ 386 | function local_moodlecheck_functiondescription(local_moodlecheck_file $file) { 387 | $errors = array(); 388 | foreach ($file->get_functions() as $function) { 389 | if ($function->phpdocs !== false && !strlen($function->phpdocs->get_description())) { 390 | $errors[] = array( 391 | 'line' => $function->phpdocs->get_line_number($file), 392 | 'object' => $function->name 393 | ); 394 | } 395 | } 396 | return $errors; 397 | } 398 | 399 | /** 400 | * Checks that all functions have proper arguments in phpdocs 401 | * 402 | * @param local_moodlecheck_file $file 403 | * @return array of found errors 404 | */ 405 | function local_moodlecheck_functionarguments(local_moodlecheck_file $file) { 406 | $errors = array(); 407 | foreach ($file->get_functions() as $function) { 408 | if ($function->phpdocs !== false) { 409 | $documentedarguments = $function->phpdocs->get_params(); 410 | $match = (count($documentedarguments) == count($function->arguments)); 411 | for ($i = 0; $match && $i < count($documentedarguments); $i++) { 412 | if (count($documentedarguments[$i]) < 2) { 413 | // Must be at least type and parameter name. 414 | $match = false; 415 | } else { 416 | $expectedtype = $function->arguments[$i][0]; 417 | $expectedparam = $function->arguments[$i][1]; 418 | $documentedtype = $documentedarguments[$i][0]; 419 | $documentedparam = $documentedarguments[$i][1]; 420 | 421 | // Documented types can be a collection (| separated). 422 | foreach (explode('|', $documentedtype) as $documentedtype) { 423 | 424 | // Ignore null. They cannot match any type in function. 425 | if (trim( $documentedtype) === 'null') { 426 | continue; 427 | } 428 | 429 | if (strpos($documentedtype, '\\') !== false) { 430 | // Namespaced typehint, potentially sub-namespaced. 431 | // We need to strip namespacing as this area just isn't that smart. 432 | $documentedtype = substr($documentedtype, strrpos($documentedtype, '\\') + 1); 433 | } 434 | 435 | if (strlen($expectedtype) && $expectedtype !== $documentedtype) { 436 | // It could be a type hinted array. 437 | if ($expectedtype !== 'array' || substr($documentedtype, -2) !== '[]') { 438 | $match = false; 439 | } 440 | } else if ($documentedtype === 'type') { 441 | $match = false; 442 | } else if ($expectedparam !== $documentedparam) { 443 | $match = false; 444 | } 445 | } 446 | } 447 | } 448 | $documentedreturns = $function->phpdocs->get_params('return'); 449 | for ($i = 0; $match && $i < count($documentedreturns); $i++) { 450 | if (empty($documentedreturns[$i][0]) || $documentedreturns[$i][0] == 'type') { 451 | $match = false; 452 | } 453 | } 454 | if (!$match) { 455 | $errors[] = array( 456 | 'line' => $function->phpdocs->get_line_number($file, '@param'), 457 | 'function' => $function->fullname); 458 | } 459 | } 460 | } 461 | return $errors; 462 | } 463 | 464 | /** 465 | * Checks that all variables have proper \var token in phpdoc block 466 | * 467 | * @param local_moodlecheck_file $file 468 | * @return array of found errors 469 | */ 470 | function local_moodlecheck_variableshasvar(local_moodlecheck_file $file) { 471 | $errors = array(); 472 | foreach ($file->get_variables() as $variable) { 473 | if ($variable->phpdocs !== false) { 474 | $documentedvars = $variable->phpdocs->get_params('var', 2); 475 | if (!count($documentedvars) || $documentedvars[0][0] == 'type') { 476 | $errors[] = array( 477 | 'line' => $variable->phpdocs->get_line_number($file, '@var'), 478 | 'variable' => $variable->fullname); 479 | } 480 | } 481 | } 482 | return $errors; 483 | } 484 | 485 | /** 486 | * Checks that all define statement have constant name in phpdoc block 487 | * 488 | * @param local_moodlecheck_file $file 489 | * @return array of found errors 490 | */ 491 | function local_moodlecheck_definedoccorrect(local_moodlecheck_file $file) { 492 | $errors = array(); 493 | foreach ($file->get_defines() as $object) { 494 | if ($object->phpdocs !== false) { 495 | if (!preg_match('/^\s*'.$object->name.'\s+-\s+(.*)/', $object->phpdocs->get_description(), $matches) || 496 | !strlen(trim($matches[1]))) { 497 | $errors[] = array('line' => $object->phpdocs->get_line_number($file), 'object' => $object->fullname); 498 | } 499 | } 500 | } 501 | return $errors; 502 | } 503 | 504 | /** 505 | * Makes sure that files have copyright tag 506 | * 507 | * @param local_moodlecheck_file $file 508 | * @return array of found errors 509 | */ 510 | function local_moodlecheck_filehascopyright(local_moodlecheck_file $file) { 511 | $phpdocs = $file->find_file_phpdocs(); 512 | if ($phpdocs && !count($phpdocs->get_tags('copyright', true))) { 513 | return array(array('line' => $phpdocs->get_line_number($file, '@copyright'))); 514 | } 515 | return array(); 516 | } 517 | 518 | /** 519 | * Makes sure that files have license tag 520 | * 521 | * @param local_moodlecheck_file $file 522 | * @return array of found errors 523 | */ 524 | function local_moodlecheck_filehaslicense(local_moodlecheck_file $file) { 525 | $phpdocs = $file->find_file_phpdocs(); 526 | if ($phpdocs && !count($phpdocs->get_tags('license', true))) { 527 | return array(array('line' => $phpdocs->get_line_number($file, '@license'))); 528 | } 529 | return array(); 530 | } 531 | -------------------------------------------------------------------------------- /file.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * File handling in moodlecheck 19 | * 20 | * @package local_moodlecheck 21 | * @copyright 2012 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | /** 28 | * Handles one file being validated 29 | * 30 | * @package local_moodlecheck 31 | * @copyright 2012 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class local_moodlecheck_file { 35 | protected $filepath = null; 36 | protected $needsvalidation = null; 37 | protected $errors = null; 38 | protected $tokens = null; 39 | protected $tokenscount = 0; 40 | protected $classes = null; 41 | protected $interfaces = null; 42 | protected $traits = null; 43 | protected $functions = null; 44 | protected $filephpdocs = null; 45 | protected $allphpdocs = null; 46 | protected $variables = null; 47 | protected $defines = null; 48 | protected $constants = null; 49 | 50 | /** 51 | * Creates an object from path to the file 52 | * 53 | * @param string $filepath 54 | */ 55 | public function __construct($filepath) { 56 | $this->filepath = str_replace(DIRECTORY_SEPARATOR, "/", $filepath); 57 | } 58 | 59 | /** 60 | * Cleares all cached stuff to free memory 61 | */ 62 | protected function clear_memory() { 63 | $this->tokens = null; 64 | $this->tokenscount = 0; 65 | $this->classes = null; 66 | $this->interfaces = null; 67 | $this->traits = null; 68 | $this->functions = null; 69 | $this->filephpdocs = null; 70 | $this->allphpdocs = null; 71 | $this->variables = null; 72 | $this->defines = null; 73 | $this->constants = null; 74 | } 75 | 76 | /** 77 | * Returns true if this file is inside specified directory 78 | * 79 | * @param string $dirpath 80 | * @return bool 81 | */ 82 | public function is_in_dir($dirpath) { 83 | // Normalize dir path to also work with Windows style directory separators... 84 | $dirpath = str_replace(DIRECTORY_SEPARATOR, "/", $dirpath); 85 | if (substr($dirpath, -1) != '/') { 86 | $dirpath .= '/'; 87 | } 88 | return substr($this->filepath, 0, strlen($dirpath)) == $dirpath; 89 | } 90 | 91 | /** 92 | * Retuns true if the file needs validation (is PHP file) 93 | * 94 | * @return bool 95 | */ 96 | public function needs_validation() { 97 | if ($this->needsvalidation === null) { 98 | $this->needsvalidation = true; 99 | $pathinfo = pathinfo($this->filepath); 100 | if (empty($pathinfo['extension']) || ($pathinfo['extension'] != 'php' && $pathinfo['extension'] != 'inc')) { 101 | $this->needsvalidation = false; 102 | } 103 | } 104 | return $this->needsvalidation; 105 | } 106 | 107 | /** 108 | * Validates a file over registered rules and returns an array of errors 109 | * 110 | * @return array 111 | */ 112 | public function validate() { 113 | if ($this->errors !== null) { 114 | return $this->errors; 115 | } 116 | $this->errors = array(); 117 | if (!$this->needs_validation()) { 118 | return $this->errors; 119 | } 120 | foreach (local_moodlecheck_registry::get_enabled_rules() as $code => $rule) { 121 | $ruleerrors = $rule->validatefile($this); 122 | if (count($ruleerrors)) { 123 | $this->errors = array_merge($this->errors, $ruleerrors); 124 | } 125 | } 126 | $this->clear_memory(); 127 | return $this->errors; 128 | } 129 | 130 | /** 131 | * Return the filepath of the file. 132 | * 133 | * @return string 134 | */ 135 | public function get_filepath() { 136 | return $this->filepath; 137 | } 138 | 139 | /** 140 | * Returns a file contents converted to array of tokens. 141 | * 142 | * Each token is an array with two elements: code of token and text 143 | * For simple 1-character tokens the code is -1 144 | * 145 | * @return array 146 | */ 147 | public function &get_tokens() { 148 | if ($this->tokens === null) { 149 | $source = file_get_contents($this->filepath); 150 | $this->tokens = token_get_all($source); 151 | $this->tokenscount = count($this->tokens); 152 | $inquotes = -1; 153 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 154 | if (is_string($this->tokens[$tid])) { 155 | // Simple 1-character token. 156 | $this->tokens[$tid] = array(-1, $this->tokens[$tid]); 157 | } 158 | // And now, for the purpose of this project we don't need strings with variables inside to be parsed 159 | // so when we find string in double quotes that is split into several tokens and combine all content in one token. 160 | if ($this->tokens[$tid][0] == -1 && $this->tokens[$tid][1] == '"') { 161 | if ($inquotes == -1) { 162 | $inquotes = $tid; 163 | $this->tokens[$tid][0] = T_STRING; 164 | } else { 165 | $this->tokens[$inquotes][1] .= $this->tokens[$tid][1]; 166 | $this->tokens[$tid] = array(T_WHITESPACE, ''); 167 | $inquotes = -1; 168 | } 169 | } else if ($inquotes > -1) { 170 | $this->tokens[$inquotes][1] .= $this->tokens[$tid][1]; 171 | $this->tokens[$tid] = array(T_WHITESPACE, ''); 172 | } 173 | } 174 | } 175 | return $this->tokens; 176 | } 177 | 178 | /** 179 | * Returns all artifacts (classes, interfaces, traits) found in file 180 | * 181 | * Returns 3 arrays (classes, interfaces and traits) of objects where each element represents an artifact: 182 | * ->type : token type of the artifact (T_CLASS, T_INTERFACE, T_TRAIT) 183 | * ->name : name of the artifact 184 | * ->tagpair : array of two elements: id of token { for the class and id of token } (false if not found) 185 | * ->phpdocs : phpdocs for this artifact (instance of local_moodlecheck_phpdocs or false if not found) 186 | * ->boundaries : array with ids of first and last token for this artifact. 187 | * 188 | * @return array with 3 elements (classes, interfaces & traits), each being an array. 189 | */ 190 | public function get_artifacts() { 191 | $types = array(T_CLASS, T_INTERFACE, T_TRAIT); // We are interested on these. 192 | $artifacts = array_combine($types, $types); 193 | if ($this->classes === null) { 194 | $this->classes = array(); 195 | $this->interfaces = array(); 196 | $this->traits = array(); 197 | $tokens = &$this->get_tokens(); 198 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 199 | if (isset($artifacts[$this->tokens[$tid][0]])) { 200 | if ($this->previous_nonspace_token($tid) === "::") { 201 | // Skip use of the ::class special constant. 202 | continue; 203 | } 204 | 205 | if ($this->previous_nonspace_token($tid) == 'new') { 206 | // This looks to be an anonymous class. 207 | 208 | if ($this->next_nonspace_token($tid) == '{') { 209 | // An anonymous class in the format `new class {`. 210 | continue; 211 | } 212 | 213 | if ($this->next_nonspace_token($tid) == 'extends') { 214 | // An anonymous class in the format `new class extends otherclasses {`. 215 | continue; 216 | } 217 | 218 | if ($this->next_nonspace_token($tid) == 'implements') { 219 | // An anonymous class in the format `new class implements someinterface {`. 220 | continue; 221 | } 222 | } 223 | $artifact = new stdClass(); 224 | $artifact->type = $artifacts[$this->tokens[$tid][0]]; 225 | $artifact->tid = $tid; 226 | $artifact->name = $this->next_nonspace_token($tid); 227 | $artifact->phpdocs = $this->find_preceeding_phpdoc($tid); 228 | $artifact->tagpair = $this->find_tag_pair($tid, '{', '}'); 229 | $artifact->boundaries = $this->find_object_boundaries($artifact); 230 | switch ($artifact->type) { 231 | case T_CLASS: 232 | $this->classes[] = $artifact; 233 | break; 234 | case T_INTERFACE: 235 | $this->interfaces[] = $artifact; 236 | break; 237 | case T_TRAIT: 238 | $this->traits[] = $artifact; 239 | break; 240 | } 241 | } 242 | } 243 | } 244 | return array(T_CLASS => $this->classes, T_INTERFACE => $this->interfaces, T_TRAIT => $this->traits); 245 | } 246 | 247 | /** 248 | * Returns all classes found in file 249 | * 250 | * Returns array of objects where each element represents a class: 251 | * $class->name : name of the class 252 | * $class->tagpair : array of two elements: id of token { for the class and id of token } (false if not found) 253 | * $class->phpdocs : phpdocs for this class (instance of local_moodlecheck_phpdocs or false if not found) 254 | * $class->boundaries : array with ids of first and last token for this class 255 | */ 256 | public function &get_classes() { 257 | return $this->get_artifacts()[T_CLASS]; 258 | } 259 | 260 | /** 261 | * Returns all functions (including class methods) found in file 262 | * 263 | * Returns array of objects where each element represents a function: 264 | * $function->tid : token id of the token 'function' 265 | * $function->name : name of the function 266 | * $function->phpdocs : phpdocs for this function (instance of local_moodlecheck_phpdocs or false if not found) 267 | * $function->class : containing class object (false if this is not a class method) 268 | * $function->fullname : name of the function with class name (if applicable) 269 | * $function->accessmodifiers : tokens like static, public, protected, abstract, etc. 270 | * $function->tagpair : array of two elements: id of token { for the function and id of token } (false if not found) 271 | * $function->argumentstoken : array of tokens found inside function arguments 272 | * $function->arguments : array of function arguments where each element is array(typename, variablename) 273 | * $function->boundaries : array with ids of first and last token for this function 274 | * 275 | * @return array 276 | */ 277 | public function &get_functions() { 278 | if ($this->functions === null) { 279 | $this->functions = array(); 280 | $tokens = &$this->get_tokens(); 281 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 282 | if ($this->tokens[$tid][0] == T_FUNCTION) { 283 | $function = new stdClass(); 284 | $function->tid = $tid; 285 | $function->fullname = $function->name = $this->next_nonspace_token($tid, false, array('&')); 286 | 287 | // Skip anonymous functions. 288 | if ($function->name == '(') { 289 | continue; 290 | } 291 | $function->phpdocs = $this->find_preceeding_phpdoc($tid); 292 | $function->class = $this->is_inside_class($tid); 293 | if ($function->class !== false) { 294 | $function->fullname = $function->class->name . '::' . $function->name; 295 | } 296 | $function->accessmodifiers = $this->find_access_modifiers($tid); 297 | if (!in_array(T_ABSTRACT, $function->accessmodifiers)) { 298 | $function->tagpair = $this->find_tag_pair($tid, '{', '}'); 299 | } else { 300 | $function->tagpair = false; 301 | } 302 | $argumentspair = $this->find_tag_pair($tid, '(', ')', array('{', ';')); 303 | if ($argumentspair !== false && $argumentspair[1] - $argumentspair[0] > 1) { 304 | $function->argumentstokens = $this->break_tokens_by( 305 | array_slice($tokens, $argumentspair[0] + 1, $argumentspair[1] - $argumentspair[0] - 1) ); 306 | } else { 307 | $function->argumentstokens = array(); 308 | } 309 | $function->arguments = array(); 310 | foreach ($function->argumentstokens as $argtokens) { 311 | $type = null; 312 | $variable = null; 313 | $splat = false; 314 | for ($j = 0; $j < count($argtokens); $j++) { 315 | if ($argtokens[$j][0] == T_VARIABLE) { 316 | $variable = ($splat) ? '...'.$argtokens[$j][1] : $argtokens[$j][1]; 317 | break; 318 | } else if ($argtokens[$j][0] != T_WHITESPACE && 319 | $argtokens[$j][0] != T_ELLIPSIS && $argtokens[$j][1] != '&') { 320 | $type = $argtokens[$j][1]; 321 | } else if ($argtokens[$j][0] == T_ELLIPSIS) { 322 | // Variadic function. 323 | $splat = true; 324 | } 325 | } 326 | 327 | // PHP 8 treats namespaces as single token. So we are going to undo this here 328 | // and continue returning only the final part of the namespace. Someday we'll 329 | // move to use full namespaces here, but not for now (we are doing the same, 330 | // in other parts of the code, when processing phpdoc blocks). 331 | if (strpos($type, '\\') !== false) { 332 | // Namespaced typehint, potentially sub-namespaced. 333 | // We need to strip namespacing as this area just isn't that smart. 334 | $type = substr($type, strrpos($type, '\\') + 1); 335 | } 336 | 337 | $function->arguments[] = array($type, $variable); 338 | } 339 | $function->boundaries = $this->find_object_boundaries($function); 340 | $this->functions[] = $function; 341 | } 342 | } 343 | } 344 | return $this->functions; 345 | } 346 | 347 | /** 348 | * Returns all class properties (variables) found in file 349 | * 350 | * Returns array of objects where each element represents a variable: 351 | * $variable->tid : token id of the token with variable name 352 | * $variable->name : name of the variable (starts with $) 353 | * $variable->phpdocs : phpdocs for this variable (instance of local_moodlecheck_phpdocs or false if not found) 354 | * $variable->class : containing class object 355 | * $variable->fullname : name of the variable with class name (i.e. classname::$varname) 356 | * $variable->accessmodifiers : tokens like static, public, protected, abstract, etc. 357 | * $variable->boundaries : array with ids of first and last token for this variable 358 | * 359 | * @return array 360 | */ 361 | public function &get_variables() { 362 | if ($this->variables === null) { 363 | $this->variables = array(); 364 | $this->get_tokens(); 365 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 366 | if ($this->tokens[$tid][0] == T_VARIABLE && ($class = $this->is_inside_class($tid)) && 367 | !$this->is_inside_function($tid)) { 368 | $variable = new stdClass; 369 | $variable->tid = $tid; 370 | $variable->name = $this->tokens[$tid][1]; 371 | $variable->class = $class; 372 | $variable->fullname = $class->name . '::' . $variable->name; 373 | $variable->accessmodifiers = $this->find_access_modifiers($tid); 374 | $variable->phpdocs = $this->find_preceeding_phpdoc($tid); 375 | $variable->boundaries = $this->find_object_boundaries($variable); 376 | $this->variables[] = $variable; 377 | } 378 | } 379 | } 380 | return $this->variables; 381 | } 382 | 383 | /** 384 | * Returns all constants found in file 385 | * 386 | * Returns array of objects where each element represents a constant: 387 | * $variable->tid : token id of the token with variable name 388 | * $variable->name : name of the variable (starts with $) 389 | * $variable->phpdocs : phpdocs for this variable (instance of local_moodlecheck_phpdocs or false if not found) 390 | * $variable->class : containing class object 391 | * $variable->fullname : name of the variable with class name (i.e. classname::$varname) 392 | * $variable->boundaries : array with ids of first and last token for this constant 393 | * 394 | * @return array 395 | */ 396 | public function &get_constants() { 397 | if ($this->constants === null) { 398 | $this->constants = array(); 399 | $this->get_tokens(); 400 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 401 | if ($this->tokens[$tid][0] == T_CONST && !$this->is_inside_function($tid)) { 402 | $variable = new stdClass; 403 | $variable->tid = $tid; 404 | $variable->fullname = $variable->name = $this->next_nonspace_token($tid, false); 405 | $variable->class = $this->is_inside_class($tid); 406 | if ($variable->class !== false) { 407 | $variable->fullname = $variable->class->name . '::' . $variable->name; 408 | } 409 | $variable->phpdocs = $this->find_preceeding_phpdoc($tid); 410 | $variable->boundaries = $this->find_object_boundaries($variable); 411 | $this->constants[] = $variable; 412 | } 413 | } 414 | } 415 | return $this->constants; 416 | } 417 | 418 | /** 419 | * Returns all 'define' statements found in file 420 | * 421 | * Returns array of objects where each element represents a define statement: 422 | * $variable->tid : token id of the token with variable name 423 | * $variable->name : name of the variable (starts with $) 424 | * $variable->phpdocs : phpdocs for this variable (instance of local_moodlecheck_phpdocs or false if not found) 425 | * $variable->class : containing class object 426 | * $variable->fullname : name of the variable with class name (i.e. classname::$varname) 427 | * $variable->boundaries : array with ids of first and last token for this constant 428 | * 429 | * @return array 430 | */ 431 | public function &get_defines() { 432 | if ($this->defines === null) { 433 | $this->defines = array(); 434 | $this->get_tokens(); 435 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 436 | if ($this->tokens[$tid][0] == T_STRING && $this->tokens[$tid][1] == 'define' && 437 | !$this->is_inside_function($tid) && !$this->is_inside_class($tid)) { 438 | $next1id = $this->next_nonspace_token($tid, true); 439 | $next1 = $this->next_nonspace_token($tid, false); 440 | $next2 = $this->next_nonspace_token($next1id, false); 441 | $variable = new stdClass; 442 | $variable->tid = $tid; 443 | if ($next1 == '(' && preg_match("/^(['\"])(.*)\\1$/", $next2, $matches)) { 444 | $variable->fullname = $variable->name = $matches[2]; 445 | } 446 | $variable->phpdocs = $this->find_preceeding_phpdoc($tid); 447 | $variable->boundaries = $this->find_object_boundaries($variable); 448 | $defines[] = $variable; 449 | } 450 | } 451 | } 452 | return $this->defines; 453 | } 454 | 455 | /** 456 | * Finds and returns object boundaries 457 | * 458 | * $obj is an object representing function, class or variable. This function 459 | * returns token ids for the very first token applicable to this object 460 | * to the very last 461 | * 462 | * @param stdClass $obj 463 | * @return array 464 | */ 465 | public function find_object_boundaries($obj) { 466 | $boundaries = array($obj->tid, $obj->tid); 467 | $tokens = &$this->get_tokens(); 468 | if (!empty($obj->tagpair)) { 469 | $boundaries[1] = $obj->tagpair[1]; 470 | } else { 471 | // Find the next ; char. 472 | for ($i = $boundaries[1]; $i < $this->tokenscount; $i++) { 473 | if ($tokens[$i][1] == ';') { 474 | $boundaries[1] = $i; 475 | break; 476 | } 477 | } 478 | } 479 | if (isset($obj->phpdocs) && $obj->phpdocs instanceof local_moodlecheck_phpdocs) { 480 | $boundaries[0] = $obj->phpdocs->get_original_token_id(); 481 | } else { 482 | // Walk back until we meet one of the characters that means that we are outside of the object. 483 | for ($i = $boundaries[0] - 1; $i >= 0; $i--) { 484 | $token = $tokens[$i]; 485 | if (in_array($token[0], array(T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG))) { 486 | break; 487 | } else if (in_array($token[1], array('{', '}', '(', ';', ',', '['))) { 488 | break; 489 | } 490 | } 491 | // Walk forward to the next meaningful token skipping all spaces and comments. 492 | for ($i = $i + 1; $i < $boundaries[0]; $i++) { 493 | if (!in_array($tokens[$i][0], array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT))) { 494 | break; 495 | } 496 | } 497 | $boundaries[0] = $i; 498 | } 499 | return $boundaries; 500 | } 501 | 502 | /** 503 | * Checks if the token with id $tid in inside some class 504 | * 505 | * @param int $tid 506 | * @return stdClass|false containing class or false if this is not a member 507 | */ 508 | public function is_inside_class($tid) { 509 | $classes = &$this->get_classes(); 510 | $classescnt = count($classes); 511 | for ($clid = 0; $clid < $classescnt; $clid++) { 512 | if ($classes[$clid]->boundaries[0] <= $tid && $classes[$clid]->boundaries[1] >= $tid) { 513 | return $classes[$clid]; 514 | } 515 | } 516 | return false; 517 | } 518 | 519 | /** 520 | * Checks if the token with id $tid in inside some function or class method 521 | * 522 | * @param int $tid 523 | * @return stdClass|false containing function or false if this is not inside a function 524 | */ 525 | public function is_inside_function($tid) { 526 | $functions = &$this->get_functions(); 527 | $functionscnt = count($functions); 528 | for ($fid = 0; $fid < $functionscnt; $fid++) { 529 | if ($functions[$fid]->boundaries[0] <= $tid && $functions[$fid]->boundaries[1] >= $tid) { 530 | return $functions[$fid]; 531 | } 532 | } 533 | return false; 534 | } 535 | 536 | /** 537 | * Checks if token with id $tid is a whitespace 538 | * 539 | * @param int $tid 540 | * @return boolean 541 | */ 542 | public function is_whitespace_token($tid) { 543 | $this->get_tokens(); 544 | return ($this->tokens[$tid][0] == T_WHITESPACE); 545 | } 546 | 547 | /** 548 | * Returns how many line feeds are in this token 549 | * 550 | * @param int $tid 551 | * @return int 552 | */ 553 | public function is_multiline_token($tid) { 554 | $this->get_tokens(); 555 | return substr_count($this->tokens[$tid][1], "\n"); 556 | } 557 | 558 | /** 559 | * Returns the first token which is not whitespace following the token with id $tid 560 | * 561 | * Also returns false if no meaningful token found till the end of file 562 | * 563 | * @param int $tid 564 | * @param bool $returnid 565 | * @param array $alsoignore 566 | * @return int|false 567 | */ 568 | public function next_nonspace_token($tid, $returnid = false, $alsoignore = array()) { 569 | $this->get_tokens(); 570 | for ($i = $tid + 1; $i < $this->tokenscount; $i++) { 571 | if (!$this->is_whitespace_token($i) && !in_array($this->tokens[$i][1], $alsoignore)) { 572 | if ($returnid) { 573 | return $i; 574 | } else { 575 | return $this->tokens[$i][1]; 576 | } 577 | } 578 | } 579 | return false; 580 | } 581 | 582 | /** 583 | * Returns the first token which is not whitespace before the token with id $tid 584 | * 585 | * Also returns false if no meaningful token found till the beggining of file 586 | * 587 | * @param int $tid 588 | * @param bool $returnid 589 | * @param array $alsoignore 590 | * @return int|false 591 | */ 592 | public function previous_nonspace_token($tid, $returnid = false, $alsoignore = array()) { 593 | $this->get_tokens(); 594 | for ($i = $tid - 1; $i > 0; $i--) { 595 | if (!$this->is_whitespace_token($i) && !in_array($this->tokens[$i][1], $alsoignore)) { 596 | if ($returnid) { 597 | return $i; 598 | } else { 599 | return $this->tokens[$i][1]; 600 | } 601 | } 602 | } 603 | return false; 604 | } 605 | 606 | /** 607 | * Returns all modifiers (private, public, static, ...) preceeding token with id $tid 608 | * 609 | * @param int $tid 610 | * @return array 611 | */ 612 | public function find_access_modifiers($tid) { 613 | $tokens = &$this->get_tokens(); 614 | $modifiers = array(); 615 | for ($i = $tid - 1; $i >= 0; $i--) { 616 | if ($this->is_whitespace_token($i)) { 617 | // Skip. 618 | continue; 619 | } else if (in_array($tokens[$i][0], 620 | array(T_ABSTRACT, T_PRIVATE, T_PUBLIC, T_PROTECTED, T_STATIC, T_VAR, T_FINAL, T_CONST))) { 621 | $modifiers[] = $tokens[$i][0]; 622 | } else { 623 | break; 624 | } 625 | } 626 | return $modifiers; 627 | } 628 | 629 | /** 630 | * Finds phpdocs preceeding the token with id $tid 631 | * 632 | * skips words abstract, private, public, protected and non-multiline whitespaces 633 | * 634 | * @param int $tid 635 | * @return local_moodlecheck_phpdocs|false 636 | */ 637 | public function find_preceeding_phpdoc($tid) { 638 | $tokens = &$this->get_tokens(); 639 | $modifiers = $this->find_access_modifiers($tid); 640 | for ($i = $tid - 1; $i >= 0; $i--) { 641 | if ($this->is_whitespace_token($i)) { 642 | if ($this->is_multiline_token($i) > 1) { 643 | // More that one line feed means that no phpdocs for this element exists. 644 | return false; 645 | } 646 | } else if ($tokens[$i][0] == T_DOC_COMMENT) { 647 | return $this->get_phpdocs($i); 648 | } else if (in_array($tokens[$i][0], $modifiers)) { 649 | // Just skip. 650 | continue; 651 | } else if (in_array($tokens[$i][1], array('{', '}', ';'))) { 652 | // This means that no phpdocs exists. 653 | return false; 654 | } else if ($tokens[$i][0] == T_COMMENT) { 655 | // This probably needed to be doc_comment. 656 | return false; 657 | } else { 658 | // No idea what it is! 659 | // TODO: change to debugging 660 | // echo "************ Unknown preceeding token id = {$tokens[$i][0]}, text = '{$tokens[$i][1]}' **************
". 661 | return false; 662 | } 663 | } 664 | return false; 665 | } 666 | 667 | /** 668 | * Finds the next pair of matching open and close symbols (usually some sort of brackets) 669 | * 670 | * @param int $startid id of token where we start looking from 671 | * @param string $opensymbol opening symbol (, { or [ 672 | * @param string $closesymbol closing symbol ), } or ] respectively 673 | * @param array $breakifmeet array of symbols that are not allowed not preceed the $opensymbol 674 | * @return array|false array of ids of two corresponding tokens or false if not found 675 | */ 676 | public function find_tag_pair($startid, $opensymbol, $closesymbol, $breakifmeet = array()) { 677 | $openid = false; 678 | $counter = 0; 679 | // Also break if we find closesymbol before opensymbol. 680 | $breakifmeet[] = $closesymbol; 681 | for ($i = $startid; $i < $this->tokenscount; $i++) { 682 | if ($openid === false && in_array($this->tokens[$i][1], $breakifmeet)) { 683 | return false; 684 | } else if ($openid !== false && $this->tokens[$i][1] == $closesymbol) { 685 | $counter--; 686 | if ($counter == 0) { 687 | return array($openid, $i); 688 | } 689 | } else if ($this->tokens[$i][1] == $opensymbol) { 690 | if ($openid === false) { 691 | $openid = $i; 692 | } 693 | $counter++; 694 | } 695 | } 696 | return false; 697 | } 698 | 699 | /** 700 | * Finds the next pair of matching open and close symbols (usually some sort of brackets) 701 | * 702 | * @param array $tokens array of tokens to parse 703 | * @param int $startid id of token where we start looking from 704 | * @param string $opensymbol opening symbol (, { or [ 705 | * @param string $closesymbol closing symbol ), } or ] respectively 706 | * @param array $breakifmeet array of symbols that are not allowed not preceed the $opensymbol 707 | * @return array|false array of ids of two corresponding tokens or false if not found 708 | */ 709 | public function find_tag_pair_inlist(&$tokens, $startid, $opensymbol, $closesymbol, $breakifmeet = array()) { 710 | $openid = false; 711 | $counter = 0; 712 | // Also break if we find closesymbol before opensymbol. 713 | $breakifmeet[] = $closesymbol; 714 | $tokenscount = count($tokens); 715 | for ($i = $startid; $i < $tokenscount; $i++) { 716 | if ($openid === false && in_array($tokens[$i][1], $breakifmeet)) { 717 | return false; 718 | } else if ($openid !== false && $tokens[$i][1] == $closesymbol) { 719 | $counter--; 720 | if ($counter == 0) { 721 | return array($openid, $i); 722 | } 723 | } else if ($tokens[$i][1] == $opensymbol) { 724 | if ($openid === false) { 725 | $openid = $i; 726 | } 727 | $counter++; 728 | } 729 | } 730 | return false; 731 | } 732 | 733 | /** 734 | * Locates the file-level phpdocs and returns it 735 | * 736 | * @return string|false either the contents of phpdocs or false if not found 737 | */ 738 | public function find_file_phpdocs() { 739 | $tokens = &$this->get_tokens(); 740 | if ($this->filephpdocs === null) { 741 | $found = false; 742 | for ($tid = 0; $tid < $this->tokenscount; $tid++) { 743 | if (in_array($tokens[$tid][0], array(T_OPEN_TAG, T_WHITESPACE, T_COMMENT))) { 744 | // All allowed before the file-level phpdocs. 745 | $found = false; 746 | } else if ($tokens[$tid][0] == T_DOC_COMMENT) { 747 | $found = $tid; 748 | break; 749 | } else { 750 | // Found something else. 751 | break; 752 | } 753 | } 754 | if ($found !== false) { 755 | // Now let's check that this is not phpdocs to the next function or class or define. 756 | $nexttokenid = $this->next_nonspace_token($tid, true); 757 | if ($nexttokenid !== false) { // Still tokens to look. 758 | $nexttoken = $this->tokens[$nexttokenid]; 759 | if ($this->is_whitespace_token($tid + 1) && $this->is_multiline_token($tid + 1) > 1) { 760 | // At least one empty line follows, it's all right. 761 | $found = $tid; 762 | } else if (in_array($nexttoken[0], 763 | array(T_DOC_COMMENT, T_COMMENT, T_REQUIRE_ONCE, T_REQUIRE, T_IF, T_INCLUDE_ONCE, T_INCLUDE))) { 764 | // Something non-documentable following, ok. 765 | $found = $tid; 766 | } else if ($nexttoken[0] == T_STRING && $nexttoken[1] == 'defined') { 767 | // Something non-documentable following. 768 | $found = $tid; 769 | } else if (in_array($nexttoken[0], array(T_CLASS, T_ABSTRACT, T_INTERFACE, T_FUNCTION))) { 770 | // This is the doc comment to the following class/function. 771 | $found = false; 772 | } 773 | // TODO: change to debugging. 774 | // } else { 775 | // echo "************ " 776 | // echo "Unknown token following the first phpdocs in " 777 | // echo "{$this->filepath}: id = {$nexttoken[0]}, text = '{$nexttoken[1]}'" 778 | // echo " **************
" 779 | // }. 780 | } 781 | } 782 | $this->filephpdocs = $this->get_phpdocs($found); 783 | } 784 | return $this->filephpdocs; 785 | } 786 | 787 | /** 788 | * Returns all parsed phpdocs block found in file 789 | * 790 | * @return array 791 | */ 792 | public function &get_all_phpdocs() { 793 | if ($this->allphpdocs === null) { 794 | $this->allphpdocs = array(); 795 | $this->get_tokens(); 796 | for ($id = 0; $id < $this->tokenscount; $id++) { 797 | if (($this->tokens[$id][0] == T_DOC_COMMENT || $this->tokens[$id][0] === T_COMMENT)) { 798 | $this->allphpdocs[$id] = new local_moodlecheck_phpdocs($this->tokens[$id], $id); 799 | } 800 | } 801 | } 802 | return $this->allphpdocs; 803 | } 804 | 805 | /** 806 | * Returns one parsed phpdocs block found in file 807 | * 808 | * @param int $tid token id of phpdocs 809 | * @return local_moodlecheck_phpdocs 810 | */ 811 | public function get_phpdocs($tid) { 812 | if ($tid === false) { 813 | return false; 814 | } 815 | $this->get_all_phpdocs(); 816 | if (isset($this->allphpdocs[$tid])) { 817 | return $this->allphpdocs[$tid]; 818 | } else { 819 | return false; 820 | } 821 | } 822 | 823 | /** 824 | * Given an array of tokens breaks them into chunks by $separator 825 | * 826 | * @param array $tokens 827 | * @param string $separator one-character separator (usually comma) 828 | * @return array of arrays of tokens 829 | */ 830 | public function break_tokens_by($tokens, $separator = ',') { 831 | $rv = array(); 832 | if (!count($tokens)) { 833 | return $rv; 834 | } 835 | $rv[] = array(); 836 | for ($i = 0; $i < count($tokens); $i++) { 837 | if ($tokens[$i][1] == $separator) { 838 | $rv[] = array(); 839 | } else { 840 | $nextpair = false; 841 | if ($tokens[$i][1] == '(') { 842 | $nextpair = $this->find_tag_pair_inlist($tokens, $i, '(', ')'); 843 | } else if ($tokens[$i][1] == '[') { 844 | $nextpair = $this->find_tag_pair_inlist($tokens, $i, '[', ']'); 845 | } else if ($tokens[$i][1] == '{') { 846 | $nextpair = $this->find_tag_pair_inlist($tokens, $i, '{', '}'); 847 | } 848 | if ($nextpair !== false) { 849 | // Skip to the end of the tag pair. 850 | for ($j = $i; $j <= $nextpair[1]; $j++) { 851 | $rv[count($rv) - 1][] = $tokens[$j]; 852 | } 853 | $i = $nextpair[1]; 854 | } else { 855 | $rv[count($rv) - 1][] = $tokens[$i]; 856 | } 857 | } 858 | } 859 | // Now trim whitespaces. 860 | for ($i = 0; $i < count($rv); $i++) { 861 | if (count($rv[$i]) && $rv[$i][0][0] == T_WHITESPACE) { 862 | array_shift($rv[$i]); 863 | } 864 | if (count($rv[$i]) && $rv[$i][count($rv[$i]) - 1][0] == T_WHITESPACE) { 865 | array_pop($rv[$i]); 866 | } 867 | } 868 | return $rv; 869 | } 870 | 871 | /** 872 | * Returns line number for the token with specified id 873 | * 874 | * @param int $tid id of the token 875 | */ 876 | public function get_line_number($tid) { 877 | $tokens = &$this->get_tokens(); 878 | if (count($tokens[$tid]) > 2) { 879 | return $tokens[$tid][2]; 880 | } else if ($tid == 0) { 881 | return 1; 882 | } else { 883 | return $this->get_line_number($tid - 1) + count(preg_split('/\n/', $tokens[$tid - 1][1])) - 1; 884 | } 885 | } 886 | } 887 | 888 | /** 889 | * Handles one phpdocs 890 | * 891 | * @package local_moodlecheck 892 | * @copyright 2012 Marina Glancy 893 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 894 | */ 895 | class local_moodlecheck_phpdocs { 896 | /** @var array static property storing the list of valid, 897 | * well known, phpdocs tags, always accepted. 898 | * @link http://manual.phpdoc.org/HTMLSmartyConverter/HandS/ */ 899 | public static $validtags = array( 900 | // Behat tags. 901 | 'Given', 902 | 'Then', 903 | 'When', 904 | // PHPUnit tags. 905 | 'covers', 906 | 'coversDefaultClass', 907 | 'coversNothing', 908 | 'dataProvider', 909 | 'depends', 910 | 'group', 911 | 'requires', 912 | 'runTestsInSeparateProcesses', 913 | 'runInSeparateProcess', 914 | 'testWith', 915 | 'uses', 916 | // PHPDoc tags. 917 | 'abstract', 918 | 'access', 919 | 'author', 920 | 'category', 921 | 'copyright', 922 | 'deprecated', 923 | 'example', 924 | 'final', 925 | 'filesource', 926 | 'global', 927 | 'ignore', 928 | 'internal', 929 | 'license', 930 | 'link', 931 | 'method', 932 | 'name', 933 | 'package', 934 | 'param', 935 | 'property', 936 | 'property-read', 937 | 'property-write', 938 | 'return', 939 | 'see', 940 | 'since', 941 | 'static', 942 | 'staticvar', 943 | 'subpackage', 944 | 'throws', 945 | 'todo', 946 | 'tutorial', 947 | 'uses', 948 | 'var', 949 | 'version' 950 | ); 951 | /** @var array static property storing the list of recommended 952 | * phpdoc tags to use within Moodle phpdocs. 953 | * @link http://docs.moodle.org/dev/Coding_style */ 954 | public static $recommendedtags = array( 955 | // Behat tags. 956 | 'Given', 957 | 'Then', 958 | 'When', 959 | // PHPUnit tags. 960 | 'covers', 961 | 'coversDefaultClass', 962 | 'coversNothing', 963 | 'dataProvider', 964 | 'depends', 965 | 'group', 966 | 'requires', 967 | 'runTestsInSeparateProcesses', 968 | 'runInSeparateProcess', 969 | 'testWith', 970 | 'uses', 971 | // PHPDoc tags. 972 | 'author', 973 | 'category', 974 | 'copyright', 975 | 'deprecated', 976 | 'license', 977 | 'link', 978 | 'package', 979 | 'param', 980 | 'property', 981 | 'property-read', 982 | 'property-write', 983 | 'return', 984 | 'see', 985 | 'since', 986 | 'subpackage', 987 | 'throws', 988 | 'todo', 989 | 'uses', 990 | 'var' 991 | ); 992 | /** @var array static property storing the list of phpdoc tags 993 | * allowed to be used under certain directories. keys are tags, values are 994 | * arrays of allowed paths (regexp patterns). 995 | */ 996 | public static $pathrestrictedtags = array( 997 | 'Given' => array('#.*/tests/behat/.*#'), 998 | 'Then' => array('#.*/tests/behat/.*#'), 999 | 'When' => array('#.*/tests/behat/.*#'), 1000 | 'covers' => array('#.*/tests/.*_test.php#'), 1001 | 'coversDefaultClass' => array('#.*/tests/.*_test.php#'), 1002 | 'coversNothing' => array('#.*/tests/.*_test.php#'), 1003 | 'dataProvider' => array('#.*/tests/.*_test.php#'), 1004 | 'depends' => array('#.*/tests/.*_test.php#'), 1005 | 'group' => array('#.*/tests/.*_test.php#'), 1006 | 'requires' => array('#.*/tests/.*_test.php#'), 1007 | 'runTestsInSeparateProcesses' => array('#.*/tests/.*_test.php#'), 1008 | 'runInSeparateProcess' => array('#.*/tests/.*_test.php#'), 1009 | 'testWith' => array('#.*/tests/.*_test.php#'), 1010 | // Commented out: 'uses' => array('#.*/tests/.*_test.php#'), can also be out from tests (Coding style dixit). 1011 | ); 1012 | /** @var array static property storing the list of phpdoc tags 1013 | * allowed to be used inline within Moodle phpdocs. */ 1014 | public static $inlinetags = array( 1015 | 'link', 1016 | 'see' 1017 | ); 1018 | /** @var array stores the original token for this phpdocs */ 1019 | protected $originaltoken = null; 1020 | /** @var int stores id the original token for this phpdocs */ 1021 | protected $originaltid = null; 1022 | /** @var string text of phpdocs with trimmed start/end tags 1023 | * as well as * in the beginning of the lines */ 1024 | protected $trimmedtext = null; 1025 | /** @var boolean whether the phpdocs contains text after the tokens 1026 | * (possible in phpdocs but not recommended in Moodle) */ 1027 | protected $brokentext = false; 1028 | /** @var string the description found in phpdocs */ 1029 | protected $description; 1030 | /** @var array array of string where each string 1031 | * represents found token (may be also multiline) */ 1032 | protected $tokens; 1033 | 1034 | /** 1035 | * Constructor. Creates an object and parses it 1036 | * 1037 | * @param array $token corresponding token parsed from file 1038 | * @param int $tid id of token in the file 1039 | */ 1040 | public function __construct($token, $tid) { 1041 | $this->originaltoken = $token; 1042 | $this->originaltid = $tid; 1043 | if (preg_match('|^///|', $token[1])) { 1044 | $this->trimmedtext = substr($token[1], 3); 1045 | } else { 1046 | $this->trimmedtext = preg_replace(array('|^\s*/\*+|', '|\*+/\s*$|'), '', $token[1]); 1047 | $this->trimmedtext = preg_replace('|\n[ \t]*\*|', "\n", $this->trimmedtext); 1048 | } 1049 | $lines = preg_split('/\n/', $this->trimmedtext); 1050 | 1051 | $this->tokens = array(); 1052 | $this->description = ''; 1053 | $istokenline = false; 1054 | for ($i = 0; $i < count($lines); $i++) { 1055 | if (preg_match('|^\s*\@(\w+)|', $lines[$i])) { 1056 | // First line of token. 1057 | $istokenline = true; 1058 | $this->tokens[] = $lines[$i]; 1059 | } else if (strlen(trim($lines[$i])) && $istokenline) { 1060 | // Second/third line of token description. 1061 | $this->tokens[count($this->tokens) - 1] .= "\n". $lines[$i]; 1062 | } else { 1063 | // This is part of description. 1064 | if (strlen(trim($lines[$i])) && !empty($this->tokens)) { 1065 | // Some text appeared AFTER tokens. 1066 | $this->brokentext = true; 1067 | } 1068 | $this->description .= $lines[$i]."\n"; 1069 | $istokenline = false; 1070 | } 1071 | } 1072 | foreach ($this->tokens as $i => $token) { 1073 | $this->tokens[$i] = trim($token); 1074 | } 1075 | $this->description = trim($this->description); 1076 | } 1077 | 1078 | /** 1079 | * Returns all tags found in phpdocs 1080 | * 1081 | * Returns array of found tokens. Each token is an unparsed string that 1082 | * may consist of multiple lines. 1083 | * Asterisk in the beginning of the lines are trimmed out 1084 | * 1085 | * @param string $tag if specified only tokens matching this tag are returned 1086 | * in this case the token itself is excluded from string 1087 | * @param bool $nonempty if true return only non-empty tags 1088 | * @return array 1089 | */ 1090 | public function get_tags($tag = null, $nonempty = false) { 1091 | if ($tag === null) { 1092 | return $this->tokens; 1093 | } else { 1094 | $rv = array(); 1095 | foreach ($this->tokens as $token) { 1096 | if (preg_match('/^\s*\@'.$tag.'\s([^\0]*)$/', $token.' ', $matches) && (!$nonempty || strlen(trim($matches[1])))) { 1097 | $rv[] = trim($matches[1]); 1098 | } 1099 | } 1100 | return $rv; 1101 | } 1102 | } 1103 | 1104 | /** 1105 | * Returns all tags found in phpdocs 1106 | * 1107 | * @deprecated use get_tags() 1108 | * @param string $tag 1109 | * @param bool $nonempty 1110 | * @return array 1111 | */ 1112 | public function get_tokens($tag = null, $nonempty = false) { 1113 | return get_tags($tag, $nonempty); 1114 | } 1115 | 1116 | /** 1117 | * Returns the description without tokens found in phpdocs 1118 | * 1119 | * @return string 1120 | */ 1121 | public function get_description() { 1122 | return $this->description; 1123 | } 1124 | 1125 | /** 1126 | * Returns true if part of the text is after any of the tokens 1127 | * 1128 | * @return bool 1129 | */ 1130 | public function is_broken_description() { 1131 | return $this->brokentext; 1132 | } 1133 | 1134 | /** 1135 | * Returns true if this is an inline phpdoc comment (starting with three slashes) 1136 | * 1137 | * @return bool 1138 | */ 1139 | public function is_inline() { 1140 | return preg_match('|^\s*///|', $this->originaltoken[1]); 1141 | } 1142 | 1143 | /** 1144 | * Returns the original token storing this phpdocs 1145 | * 1146 | * @return array 1147 | */ 1148 | public function get_original_token() { 1149 | return $this->originaltoken; 1150 | } 1151 | 1152 | /** 1153 | * Returns the id for original token storing this phpdocs 1154 | * 1155 | * @return int 1156 | */ 1157 | public function get_original_token_id() { 1158 | return $this->originaltid; 1159 | } 1160 | 1161 | /** 1162 | * Returns short description found in phpdocs if found (first line followed by empty line) 1163 | * 1164 | * @return string 1165 | */ 1166 | public function get_shortdescription() { 1167 | $lines = preg_split('/\n/', $this->description); 1168 | if (count($lines) == 1 || (count($lines) && !strlen(trim($lines[1])))) { 1169 | return $lines[0]; 1170 | } else { 1171 | return false; 1172 | } 1173 | } 1174 | 1175 | /** 1176 | * Returns list of parsed param tokens found in phpdocs 1177 | * 1178 | * Each element is array(typename, variablename, variabledescription) 1179 | * 1180 | * @param string $tag tag name to look for. Usually param but may be var for variables 1181 | * @param int $splitlimit maximum number of chunks to return 1182 | * @return array 1183 | */ 1184 | public function get_params($tag = 'param', $splitlimit = 3) { 1185 | $params = array(); 1186 | foreach ($this->get_tags($tag) as $token) { 1187 | $params[] = preg_split('/\s+/', trim($token), $splitlimit); // AKA 'type $name multi-word description'. 1188 | } 1189 | return $params; 1190 | } 1191 | 1192 | /** 1193 | * Returns the line number where this phpdoc occurs in the file 1194 | * 1195 | * @param local_moodlecheck_file $file 1196 | * @param string $substring if specified the line number of first occurence of $substring is returned 1197 | * @return int 1198 | */ 1199 | public function get_line_number(local_moodlecheck_file $file, $substring = null) { 1200 | $line0 = $file->get_line_number($this->get_original_token_id()); 1201 | if ($substring === null) { 1202 | return $line0; 1203 | } else { 1204 | $chunks = preg_split('!' . preg_quote($substring, '!') . '!', $this->originaltoken[1]); 1205 | if (count($chunks) > 1) { 1206 | $lines = preg_split('/\n/', $chunks[0]); 1207 | return $line0 + count($lines) - 1; 1208 | } else { 1209 | return $line0; 1210 | } 1211 | } 1212 | } 1213 | 1214 | /** 1215 | * Returns all the inline tags found in the phpdoc 1216 | * 1217 | * This method returns all the phpdocs tags found inline, 1218 | * embed into the phpdocs contents. Only valid tags are 1219 | * considered See {@link self::$validtags}. 1220 | * 1221 | * @param bool $withcurly if true, only tags properly enclosed 1222 | * with curly brackets are returned. Else all the inline tags are returned. 1223 | * @param bool $withcontent if true, the contents after the tag are also returned. 1224 | * Else, the tags are returned both as key and values (for BC). 1225 | * 1226 | * @return array inline tags found in the phpdoc, with contents if specified. 1227 | */ 1228 | public function get_inline_tags($withcurly = true, $withcontent = false) { 1229 | $inlinetags = array(); 1230 | // Trim the non-inline phpdocs tags. 1231 | $text = preg_replace('|^\s*@?|m', '', $this->trimmedtext); 1232 | if ($withcurly) { 1233 | $regex = '#{@([a-z\-]*)(.*?)[}\n]#'; 1234 | } else { 1235 | $regex = '#@([a-z\-]*)(.*?)[}\n]#'; 1236 | } 1237 | if (preg_match_all($regex, $text, $matches)) { 1238 | // Filter out invalid ones, can be ignored. 1239 | foreach ($matches[1] as $key => $tag) { 1240 | if (in_array($tag, self::$validtags)) { 1241 | if ($withcontent && isset($matches[2][$key])) { 1242 | // Let's add the content. 1243 | $inlinetags[] = $tag . ' ' . trim($matches[2][$key]); 1244 | } else { 1245 | // Just the tag, without content. 1246 | $inlinetags[] = $tag; 1247 | } 1248 | } 1249 | } 1250 | } 1251 | return $inlinetags; 1252 | } 1253 | } 1254 | --------------------------------------------------------------------------------