Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

[WIP] Validates the completeness of docblocks #52

Open
wants to merge 2 commits into from

1 participant

@blainesch
Collaborator

Currently it's doing this under the syntax command, and should be moved to the documented command.

Testing to validate travis on pr's

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 428 additions and 16 deletions.
  1. +4 −0 analysis/Parser.php
  2. +5 −2 config/bootstrap.php
  3. +34 −6 extensions/command/Quality.php
  4. +5 −2 extensions/test/filter/Syntax.php
  5. +4 −0 test/Rule.php
  6. +14 −5 test/Rules.php
  7. +7 −0 test/Testable.php
  8. +75 −0 test/rules.json
  9. +12 −0 test/rules/BeginsWithOpenTag.php
  10. +3 −0  test/rules/ConstructsWithoutBrackets.php
  11. +3 −0  test/rules/ControlStructuresHaveCorrectSpacing.php
  12. +15 −0 test/rules/DoesntExceedMaxLineLength.php
  13. +3 −0  test/rules/HasCorrectCommentStyle.php
  14. +3 −0  test/rules/HasCorrectDocblockStyle.php
  15. +15 −0 test/rules/HasCorrectEncoding.php
  16. +4 −0 test/rules/HasCorrectFunctionNames.php
  17. +17 −0 test/rules/HasCorrectPermissions.php
  18. +14 −0 test/rules/HasCorrectVariableNames.php
  19. +4 −0 test/rules/HasExplicitPropertyAndMethodVisibility.php
  20. +17 −0 test/rules/HasNoForbiddenStatements.php
  21. +12 −0 test/rules/HasNoPrivateMethods.php
  22. +10 −0 test/rules/HasNoTrailingWhitespace.php
  23. +11 −0 test/rules/HasTabsAsIndentation.php
  24. +3 −0  test/rules/OperatorSpacing.php
  25. +5 −0 test/rules/ProtectedNamesStartWithUnderscore.php
  26. +4 −0 test/rules/TabsOnlyAppearFirst.php
  27. +5 −0 test/rules/WeakComparisonOperators.php
  28. +119 −0 test/rules/docblock/Completeness.php
  29. +1 −1  tests/mocks/test/rules/MockHasCorrectPermissions.php
View
4 analysis/Parser.php
@@ -4,6 +4,10 @@
use li3_quality\analysis\ParserException;
+/**
+ * The parser class uses PHP's tokenizer to provide methods and tools for performing static analysis
+ * on PHP code.
+ */
class Parser extends \lithium\analysis\Parser {
/**
View
7 config/bootstrap.php
@@ -12,8 +12,11 @@
Libraries::paths(array(
'rules' => array(
'{:library}\extensions\test\rules\{:class}\{:name}',
- '{:library}\test\rules\{:class}\{:name}' => array('libraries' => 'li3_quality')
- )
+ '{:library}\test\rules\{:class}\{:name}' => array('libraries' => 'li3_quality'),
+ ),
+ 'ruleSets' => array(
+ '{:library}\test\*{rules,defaultRules}',
+ ),
));
if (!Multibyte::config('li3_quality')) {
View
40 extensions/command/Quality.php
@@ -22,22 +22,29 @@ class Quality extends \lithium\console\command\Test {
/**
* The library to run the quality checks on.
+ *
+ * @var int
*/
public $library = true;
/**
* If `--slient NUM` is used, only classes below this coverage are shown.
+ *
+ * @var int
*/
public $threshold = 100;
/**
* This is the minimum threshold for core tests to be green.
+ *
+ * @var int
*/
protected $_greenThreshold = 85;
/**
* Show help on run.
*
+ * @return boolean
*/
public function run($path = null) {
return $this->_help();
@@ -46,6 +53,8 @@ public function run($path = null) {
/**
* Checks the syntax of your class files through static code analysis.
* if GIT_DIR env variable is set, then use plain and silent.
+ *
+ * @return void
*/
public function syntax($path = null) {
if ($this->request->env('GIT_DIR')) {
@@ -105,6 +114,8 @@ public function syntax($path = null) {
/**
* Checks for undocumented classes or methods inside the library.
+ *
+ * @return void
*/
public function documented() {
$this->header('Lithium Documentation Check');
@@ -119,6 +130,8 @@ public function documented() {
/**
* Lists code coverage for a given threshold (100 by default).
+ *
+ * @return void
*/
public function coverage() {
$this->header('Lithium Code Coverage');
@@ -169,6 +182,8 @@ public function coverage() {
/**
* Returns a list of testable classes according to the given library.
+ *
+ * @return array
*/
protected function _testables($options = array()) {
$defaults = array('recursive' => true, 'path' => null);
@@ -200,12 +215,12 @@ protected function _syntaxFilters() {
if (!is_array($this->filters)) {
$filters = $this->filters ? array_map('trim', explode(',', $this->filters)) : array();
if (count($filters) === 0) {
- $config = Libraries::get($this->library);
- $ruleConfig = $config['path'] . '/test/rules.json';
- if (!file_exists($ruleConfig)) {
- $config = Libraries::get('li3_quality');
- $ruleConfig = $config['path'] . '/test/defaultRules.json';
- }
+ list($ruleConfig) = Libraries::locate('ruleSets', null, array(
+ 'recursive' => false,
+ 'suffix' => '.json',
+ 'format' => false,
+ 'preFilter' => '/(r|defaultR)ules\.json/',
+ ));
$ruleConfig = json_decode(file_get_contents($ruleConfig), true);
$filters = $ruleConfig['rules'];
if (isset($ruleConfig['variables'])) {
@@ -217,6 +232,19 @@ protected function _syntaxFilters() {
return $this->filters;
}
+ /**
+ * Will decode json string into an object
+ *
+ * @param string $text
+ * @return array
+ */
+ protected function _decode($text) {
+ if (($data = json_decode($text, true)) === null) {
+ throw new \Exception('JSON was not decoded correctly.');
+ }
+ return $data;
+ }
+
}
?>
View
7 extensions/test/filter/Syntax.php
@@ -12,12 +12,14 @@
use li3_quality\test\Testable;
/**
- *
+ * Syntax filter for testing
*/
class Syntax extends \lithium\test\Filter {
/**
+ * Will apply the filter
*
+ * @return array
*/
public static function apply($report, $tests, array $options = array()) {
foreach ($tests->invoke('subject') as $class) {
@@ -25,12 +27,13 @@ public static function apply($report, $tests, array $options = array()) {
$class => Rules::apply(new Testable(array('path' => $class)))
));
}
-
return $tests;
}
/**
+ * Will analyze the filter
*
+ * @return array
*/
public static function analyze($report, array $options = array()) {
$results = $report->results['filters'][__CLASS__];
View
4 test/Rule.php
@@ -7,6 +7,10 @@
*/
namespace li3_quality\test;
+/**
+ * This is the abstract rule all rules should inherit from, it gives extra
+ * functionality to the rules such as adding violations and warnings.
+ */
abstract class Rule extends \lithium\core\Object {
/**
View
19 test/Rules.php
@@ -9,6 +9,9 @@
use lithium\core\Libraries;
+/**
+ * The rules class is the collection of Rule classes.
+ */
class Rules extends \lithium\core\StaticObject {
/**
@@ -42,8 +45,15 @@ public static function __init() {
*/
public static function add($rule, $options = array()) {
$class = get_class($rule);
- $sep = strrpos($class, '\\');
- $name = ($sep !== false) ? substr($class, $sep + 1) : $class;
+ $sep = strrpos($class, 'rules\\');
+ if ($sep === false) {
+ $sep = strrpos($class, '\\');
+ $distance = $sep + 1;
+ } else {
+ $distance = $sep + 6;
+ }
+ $name = ($sep !== false) ? substr($class, $distance) : $class;
+ $name = str_replace('\\', '/', $name);
static::$_rules[$name] = array(
'rule' => $rule,
'options' => $options,
@@ -69,8 +79,7 @@ public static function apply($testable, array $filters = array()) {
foreach ($rules as $ruleSet) {
$rule = $ruleSet['rule'];
- $options = $ruleSet['options'];
- $rule->apply($testable, $options);
+ $rule->apply($testable, $ruleSet['options']);
$warnings = array_merge($warnings, $rule->warnings());
if (!$rule->success()) {
$success = false;
@@ -127,7 +136,7 @@ public static function ruleOptions(array $variables) {
$rule['options'] = $variables[$key];
}
}
- return static::$_rules;;
+ return static::$_rules;
}
}
View
7 test/Testable.php
@@ -10,6 +10,11 @@
use lithium\core\Libraries;
use li3_quality\analysis\Parser;
+/**
+ * The testable class is the object passed to each of the rules giving the rule
+ * added functionality to tokens, source code, and grouped tokens. This makes it
+ * possible to easily test rules without a ton of complex logic.
+ */
class Testable extends \lithium\core\Object {
/**
@@ -56,6 +61,8 @@ class Testable extends \lithium\core\Object {
/**
* Locates the file and reads its source code.
+ *
+ * @return void
*/
public function __construct(array $config = array()) {
$this->_config = $config + array(
View
75 test/rules.json
@@ -27,12 +27,87 @@
"WeakComparisonOperators",
"UnusedUseStatements",
"MultipleBlankLines"
+ "docblock/Completeness",
+ "docblock/Accurate"
],
"variables": {
"DoesntExceedMaxLineLength": {
"softLimit": 80,
"hardLimit": 100,
"tabWidth": 4
+ },
+ "docblock/Completeness": {
+ "excludeFiles": "/test|mock/i",
+ "page": {
+ "required": true
+ },
+ "class": {
+ "required": true,
+ "sections": [
+ "description"
+ ]
+ },
+ "variable": {
+ "required": false
+ },
+ "method": {
+ "required": true,
+ "sections": [
+ "description",
+ "tags"
+ ],
+ "tags": [
+ "return"
+ ]
+ }
+ },
+ "docblock/Accurate": {
+ "page": {
+ "sections": [
+ "description",
+ "text",
+ "tags"
+ ],
+ "tags": [
+ "copyright",
+ "license"
+ ]
+ },
+ "class": {
+ "sections": [
+ "description",
+ "text",
+ "tags"
+ ],
+ "tags": [
+ "see"
+ ]
+ },
+ "variable": {
+ "sections": [
+ "description",
+ "tags"
+ ],
+ "tags": [
+ "var"
+ ]
+ },
+ "method": {
+ "sections": [
+ "description",
+ "text",
+ "tags"
+ ],
+ "tags": [
+ "deprecated",
+ "todo",
+ "link",
+ "param",
+ "return",
+ "throws",
+ "filter"
+ ]
+ }
}
}
}
View
12 test/rules/BeginsWithOpenTag.php
@@ -8,8 +8,20 @@
namespace li3_quality\test\rules;
+use lithium\analysis\Docblock;
+
+/**
+ * Validates php files begin with the correct opening php tag.
+ */
class BeginsWithOpenTag extends \li3_quality\test\Rule {
+ /**
+ * Checks the first line for the opening php tag
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$message = "File does not begin with <?php";
$lines = $testable->lines();
View
3  test/rules/ConstructsWithoutBrackets.php
@@ -10,6 +10,9 @@
use lithium\util\String;
+/**
+ * Will validate no language constructs have brackets around them.
+ */
class ConstructsWithoutBrackets extends \li3_quality\test\Rule {
/**
View
3  test/rules/ControlStructuresHaveCorrectSpacing.php
@@ -10,6 +10,9 @@
use lithium\util\String;
+/**
+ * Validates control structures have correct spacing and indentation.
+ */
class ControlStructuresHaveCorrectSpacing extends \li3_quality\test\Rule {
/**
View
15 test/rules/DoesntExceedMaxLineLength.php
@@ -10,14 +10,29 @@
use lithium\g11n\Multibyte;
+/**
+ * Ensures the hard/soft line limits are not exceeded.
+ */
class DoesntExceedMaxLineLength extends \li3_quality\test\Rule {
+ /**
+ * The default configuration for hard/soft limits.
+ *
+ * @var array
+ */
public $config = array(
'hardLimit' => 100,
'softLimit' => 80,
'tabWidth' => 4,
);
+ /**
+ * Iterates each line checking the length of it.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
extract($config += $this->config);
foreach ($testable->lines() as $i => $line) {
View
3  test/rules/HasCorrectCommentStyle.php
@@ -10,6 +10,9 @@
use li3_quality\analysis\Parser;
+/**
+ * Validates that include comments don't appear except within test methods.
+ */
class HasCorrectCommentStyle extends \li3_quality\test\Rule {
/**
View
3  test/rules/HasCorrectDocblockStyle.php
@@ -10,6 +10,9 @@
use lithium\util\String;
+/**
+ * Validates the syntax of docblocks
+ */
class HasCorrectDocblockStyle extends \li3_quality\test\Rule {
/**
View
15 test/rules/HasCorrectEncoding.php
@@ -8,8 +8,18 @@
namespace li3_quality\test\rules;
+/**
+ * Ensures the document has the correct UTF-8 encoding.
+ */
class HasCorrectEncoding extends \li3_quality\test\Rule {
+ /**
+ * Will do a detection on the entire source for UTF-8
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$message = "File is not encoded as UTF-8";
@@ -19,6 +29,11 @@ public function apply($testable, array $config = array()) {
}
}
+ /**
+ * Disables this rule if mb_detect_encoding is not installed.
+ *
+ * @return bool
+ */
public function enabled() {
return function_exists('mb_detect_encoding');
}
View
4 test/rules/HasCorrectFunctionNames.php
@@ -11,6 +11,10 @@
use lithium\util\Inflector;
use li3_quality\analysis\Parser;
+/**
+ * Similar to HasCorrectVariableNames but checks that method names are in
+ * the correct camelBack format.
+ */
class HasCorrectFunctionNames extends \li3_quality\test\Rule {
/**
View
17 test/rules/HasCorrectPermissions.php
@@ -8,8 +8,18 @@
namespace li3_quality\test\rules;
+/**
+ * Will check that that a file is not executeable.
+ */
class HasCorrectPermissions extends \li3_quality\test\Rule {
+ /**
+ * Checks the file is executeable and if so throws a violation.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$message = "File is executable";
if ($this->_isExecutable($testable->config('path'))) {
@@ -17,6 +27,13 @@ public function apply($testable, array $config = array()) {
}
}
+ /**
+ * Detects wether the given file is executeable or not. Creates for easier
+ * unit tests.
+ *
+ * @param string $path The path to the file
+ * @return boolean
+ */
protected function _isExecutable($path) {
return is_executable($path);
}
View
14 test/rules/HasCorrectVariableNames.php
@@ -10,8 +10,15 @@
use lithium\util\Inflector;
+/**
+ * Ensures all variables are in the correct camelBack style.
+ */
class HasCorrectVariableNames extends \li3_quality\test\Rule {
+ /**
+ * Standard php variables we should ignore.
+ * @var array
+ */
protected $_superglobals = array(
'$GLOBALS' => true,
'$_SERVER' => true,
@@ -24,6 +31,13 @@ class HasCorrectVariableNames extends \li3_quality\test\Rule {
'$_ENV' => true
);
+ /**
+ * Iterates tokens looking for for variables and checking their content.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$tokens = $testable->tokens();
$filtered = $testable->findAll(array(T_VARIABLE));
View
4 test/rules/HasExplicitPropertyAndMethodVisibility.php
@@ -11,6 +11,10 @@
use lithium\util\String;
use li3_quality\analysis\Parser;
+/**
+ * Ensures every method and instance variable has declared visibility.
+ * This could possibly be combined with the HasNoPrivateMethods
+ */
class HasExplicitPropertyAndMethodVisibility extends \li3_quality\test\Rule {
/**
View
17 test/rules/HasNoForbiddenStatements.php
@@ -8,8 +8,18 @@
namespace li3_quality\test\rules;
+/**
+ * Validates that there are no incorrectly used constructs such as eval or short
+ * hand syntax.
+ */
class HasNoForbiddenStatements extends \li3_quality\test\Rule {
+ /**
+ * The list of items to search for
+ *
+ * @todo apply this in the config file
+ * @var array
+ */
protected $_forbidden = array(
T_ENDDECLARE => 'enddeclare',
T_ENDFOR => 'endfor',
@@ -24,6 +34,13 @@ class HasNoForbiddenStatements extends \li3_quality\test\Rule {
T_VAR => 'var'
);
+ /**
+ * Iterates searching for the given tokens.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$tokens = $testable->tokens();
$filtered = array_merge(array_keys($this->_forbidden), array(T_STRING));
View
12 test/rules/HasNoPrivateMethods.php
@@ -8,8 +8,20 @@
namespace li3_quality\test\rules;
+/**
+ * This rule throws violations when private methods/variables are used in your
+ * code.
+ */
class HasNoPrivateMethods extends \li3_quality\test\Rule {
+ /**
+ * Iterates all tokens simply looking for the private token, once found
+ * throws a violation.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$tokens = $testable->tokens();
$filtered = $testable->findAll(array(T_PRIVATE));
View
10 test/rules/HasNoTrailingWhitespace.php
@@ -10,8 +10,18 @@
use lithium\g11n\Multibyte;
+/**
+ * Validates there is no trailing whitespace on a line.
+ */
class HasNoTrailingWhitespace extends \li3_quality\test\Rule {
+ /**
+ * Iterates lines and testing it against a regex to determine it's success.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$message = "Trailing whitespace found";
$lines = $testable->lines();
View
11 test/rules/HasTabsAsIndentation.php
@@ -8,6 +8,9 @@
namespace li3_quality\test\rules;
+/**
+ * Will ensure the file uses tabs as intentions instead of spaces.
+ */
class HasTabsAsIndentation extends \li3_quality\test\Rule {
/**
@@ -19,6 +22,14 @@ class HasTabsAsIndentation extends \li3_quality\test\Rule {
T_ENCAPSED_AND_WHITESPACE,
);
+ /**
+ * Iterates lines validating it with a simple regex and ignoring lines with
+ * specific tokens.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
public function apply($testable, array $config = array()) {
$message = "Uses spaces instead of tabs";
$lines = $testable->lines();
View
3  test/rules/OperatorSpacing.php
@@ -10,6 +10,9 @@
use lithium\util\String;
+/**
+ * Will ensure all operators have correct spacing before and after itself.
+ */
class OperatorSpacing extends \li3_quality\test\Rule {
/**
View
5 test/rules/ProtectedNamesStartWithUnderscore.php
@@ -11,6 +11,10 @@
use lithium\util\String;
use li3_quality\analysis\Parser;
+/**
+ * Will determine if all protected names (T_FUNCTION and T_VARIABLE) have names
+ * that start with an underscore '_' character.
+ */
class ProtectedNamesStartWithUnderscore extends \li3_quality\test\Rule {
/**
@@ -18,6 +22,7 @@ class ProtectedNamesStartWithUnderscore extends \li3_quality\test\Rule {
* found it will validate the name of it's parent starts with an underscore.
*
* @param Testable $testable The testable object
+ * @param array $config
* @return void
*/
public function apply($testable, array $config = array()) {
View
4 test/rules/TabsOnlyAppearFirst.php
@@ -8,6 +8,10 @@
namespace li3_quality\test\rules;
+/**
+ * Validates that tabs only appear first on a line since spaces should be used
+ * for formatting data.
+ */
class TabsOnlyAppearFirst extends \li3_quality\test\Rule {
/**
View
5 test/rules/WeakComparisonOperators.php
@@ -10,6 +10,11 @@
use lithium\util\String;
+/**
+ * Checks for weak comparison operators and throws warnings. Weak comparison
+ * operators include '==' and '!=' which should be replaced, if possible,
+ * by their stronger counterparts '===' and '!=='.
+ */
class WeakComparisonOperators extends \li3_quality\test\Rule {
/**
View
119 test/rules/docblock/Completeness.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace li3_quality\test\rules\docblock;
+
+use lithium\analysis\Docblock;
+
+/**
+ * Will determine the completeness of the docblocks.
+ * Throws violations when missing:
+ * * Required docblocks
+ * * Required sections
+ * * Required tags
+ */
+class Completeness extends \li3_quality\test\Rule {
+
+ /**
+ * Will determine if the given file is ignoreable.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return bool
+ */
+ protected function _ignore($testable, array $config = array()) {
+ if (!isset($config['excludeFiles'])) {
+ return false;
+ }
+ return preg_match($config['excludeFiles'], $testable->config('path')) === 1;
+ }
+
+ /**
+ * Will return a list of inspectable docblock by type. Will also throw
+ * violations for missing docblock tags.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return void
+ */
+ public function apply($testable, array $config = array()) {
+ if ($this->_ignore($testable, $config)) {
+ return;
+ }
+ $docblocks = $this->inspectableDocBlocks($testable, $config);
+ $tokens = $testable->tokens();
+ foreach ($docblocks as $type => $items) {
+ $required = $config[$type];
+ foreach ($items as $item) {
+ $token = $tokens[$item];
+ $docblock = Docblock::comment($token['content']);
+ foreach ($required['sections'] as $section) {
+ if (!isset($docblock[$section])) {
+ $this->addViolation(array(
+ 'message' => 'Docblock has no required section ' . $section . '.',
+ 'line' => $token['line'],
+ ));
+ }
+ }
+ foreach ($required['tags'] as $tag) {
+ if (!isset($docblock['tags'][$tag])) {
+ $this->addViolation(array(
+ 'message' => 'Docblock has no required tag ' . $tag . '.',
+ 'line' => $token['line'],
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Will return a list of inspectable docblock by type. Will also throw
+ * violations for missing docblock tags.
+ *
+ * @param Testable $testable The testable object
+ * @param array $config
+ * @return array
+ */
+ public function inspectableDocBlocks($testable, array $config = array()) {
+ $tokens = $testable->tokens();
+ $types = $testable->typeCache();
+ $lineCache = $testable->lineCache();
+
+ $class = $types[T_CLASS];
+ $children = $tokens[$class[0]]['children'];
+ $variable = $testable->findAll(array(T_VARIABLE), $children);
+ $method = $testable->findAll(array(T_FUNCTION), $children);
+
+ $docblocks = array(
+ 'page' => $testable->findAll(array(T_DOC_COMMENT), $lineCache[2]),
+ );
+
+ foreach (compact('class', 'variable', 'method') as $type => $items) {
+ $docblocks[$type] = array();
+ foreach ($items as $item) {
+ $tokenLine = $tokens[$item]['line'];
+ $prevToken = $testable->findTokenByLine($tokenLine - 2);
+ $prevLine = $tokens[$prevToken]['line'];
+ $docblock = $testable->findNext(array(T_DOC_COMMENT), $lineCache[$prevLine]);
+ if ($docblock !== false) {
+ $docblocks[$type][] = $docblock;
+ } elseif ($config[$type]['required']) {
+ $this->addViolation(array(
+ 'message' => "Token {$tokens[$item]['name']} does not have docblocks",
+ 'line' => $tokenLine,
+ ));
+ }
+ }
+ }
+ return $docblocks;
+ }
+
+}
+
+?>
View
2  tests/mocks/test/rules/MockHasCorrectPermissions.php
@@ -5,7 +5,7 @@
use li3_quality\test\rules\HasCorrectPermissions;
/**
- * A mock of the Rule object
+ * A mock of the Rule object.
*/
class MockHasCorrectPermissions extends HasCorrectPermissions {
Something went wrong with that request. Please try again.