diff --git a/src/main/php/PDepend/Metrics/Analyzer/HalsteadAnalyzer.php b/src/main/php/PDepend/Metrics/Analyzer/HalsteadAnalyzer.php new file mode 100644 index 0000000000..de366b8abb --- /dev/null +++ b/src/main/php/PDepend/Metrics/Analyzer/HalsteadAnalyzer.php @@ -0,0 +1,386 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Manuel Pichler nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @copyright 2015 Matthias Mullie. All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +namespace PDepend\Metrics\Analyzer; + +use PDepend\Metrics\AbstractCachingAnalyzer; +use PDepend\Metrics\AnalyzerNodeAware; +use PDepend\Source\AST\AbstractASTCallable; +use PDepend\Source\AST\ASTArtifact; +use PDepend\Source\AST\ASTFunction; +use PDepend\Source\AST\ASTInterface; +use PDepend\Source\AST\ASTMethod; +use PDepend\Source\Tokenizer\Tokens; + +/** + * This class calculates the Halstead Complexity Measures for the project, + * methods and functions. + * + * @copyright 2015 Matthias Mullie. All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ +class HalsteadAnalyzer extends AbstractCachingAnalyzer implements AnalyzerNodeAware +{ + /** + * Metrics provided by the analyzer implementation. + */ + const M_HALSTEAD_LENGTH = 'hnt', // N = N1 + N2 (total operators + operands) + M_HALSTEAD_VOCABULARY = 'hnd', // n = n1 + n2 (distinct operators + operands) + M_HALSTEAD_VOLUME = 'hv', // V = N * log2(n) + M_HALSTEAD_DIFFICULTY = 'hd', // D = (n1 / 2) * (N2 / n2) + M_HALSTEAD_LEVEL = 'hl', // L = 1 / D + M_HALSTEAD_EFFORT = 'he', // E = V * D + M_HALSTEAD_TIME = 'ht', // T = E / 18 + M_HALSTEAD_BUGS = 'hb', // B = (E ** (2/3)) / 3000 + M_HALSTEAD_CONTENT = 'hi'; // I = (V / D) + + /** + * Processes all {@link \PDepend\Source\AST\ASTNamespace} code nodes. + * + * @param \PDepend\Source\AST\ASTNamespace $namespaces + * @return void + */ + public function analyze($namespaces) + { + if ($this->metrics === null) { + $this->loadCache(); + $this->fireStartAnalyzer(); + + // Init node metrics + $this->metrics = array(); + + foreach ($namespaces as $namespace) { + $namespace->accept($this); + } + + $this->fireEndAnalyzer(); + $this->unloadCache(); + } + } + + /** + * This method will return an array with all generated metric values + * for the given $node. If there are no metrics for the requested + * node, this method will return an empty array. + * + * @param \PDepend\Source\AST\ASTArtifact $artifact + * @return array + */ + public function getNodeMetrics(ASTArtifact $artifact) + { + if (isset($this->metrics[$artifact->getId()])) { + $basis = $this->metrics[$artifact->getId()]; + + return $this->calculateHalsteadMeasures($basis); + } + + return array(); + } + + /** + * Visits a function node. + * + * @param \PDepend\Source\AST\ASTFunction $function + * @return void + */ + public function visitFunction(ASTFunction $function) + { + $this->fireStartFunction($function); + + if (false === $this->restoreFromCache($function)) { + $this->calculateHalsteadBasis($function); + } + + $this->fireEndFunction($function); + } + + /** + * Visits a code interface object. + * + * @param \PDepend\Source\AST\ASTInterface $interface + * @return void + */ + public function visitInterface(ASTInterface $interface) + { + // Empty visit method, we don't want interface metrics + } + + /** + * Visits a method node. + * + * @param \PDepend\Source\AST\ASTMethod $method + * @return void + */ + public function visitMethod(ASTMethod $method) + { + $this->fireStartMethod($method); + + if (false === $this->restoreFromCache($method)) { + $this->calculateHalsteadBasis($method); + } + + $this->fireEndMethod($method); + } + + /** + * @see http://www.scribd.com/doc/99533/Halstead-s-Operators-and-Operands-in-C-C-JAVA-by-Indranil-Nandy + * + * @param \PDepend\Source\AST\AbstractASTCallable $callable + * @return void + */ + public function calculateHalsteadBasis(AbstractASTCallable $callable) + { + $operators = array(); + $operands = array(); + + $skipUntil = null; + + $tokens = $callable->getTokens(); + foreach ($tokens as $i => $token) { + /* + * Some operations should be ignored, e.g. function declarations. + * When we encounter a new function, we'll skip all tokens until we + * find the closing token. + */ + if ($skipUntil !== null) { + if ($token->type === $skipUntil) { + $skipUntil = null; + } + + continue; + } + + switch ($token->type) { + // A pair of parenthesis is considered a single operator. + case Tokens::T_PARENTHESIS_CLOSE: + case Tokens::T_CURLY_BRACE_CLOSE: + case Tokens::T_SQUARED_BRACKET_CLOSE: + case Tokens::T_ANGLE_BRACKET_CLOSE: + break; + + // A label is considered an operator if it is used as the target + // of a GOTO statement. + case Tokens::T_GOTO: + $operators[] = $token->image; + // Ignore next token as operand but count as operator instead. + $skipUntil = $tokens[$i + 1]->type; + $operators[] = $tokens[$i + 1]->image; + break; + + /* + * The following control structures case ...: for (...) if (...) + * switch (...) while(...) and try-catch (...) are treated in a + * special way. The colon and the parentheses are considered to + * be a part of the constructs. The case and the colon or the + * “for (...)”, “if (...)”, “switch (...)”, “while(...)”, + * “try-catch( )” are counted together as one operator. + */ + case Tokens::T_IF: + case Tokens::T_FOR: + case Tokens::T_FOREACH: + case Tokens::T_WHILE: + case Tokens::T_CATCH: + // case Tokens::T_SWITCH: // not followed by () + // case Tokens::T_TRY: // not followed by () + // case Tokens::T_DO: // always come with while, which accounts for () already + $operators[] = $token->image; + /* + * These are always followed by parenthesis, which would add + * another operator (only opening parenthesis counts) + * so we'll have to skip that one. + */ + $skipUntil = Tokens::T_PARENTHESIS_OPEN; + $operators[] = $token->image; + break; + + /* + * The ternary operator ‘?’ followed by ‘:’ is considered a + * single operator as it is equivalent to “if-else” construct. + */ + case Tokens::T_COLON: + /* + * Colon is used after keyword, where it counts as part of + * that operator, or in ternary operator, where it also + * counts as 1. + */ + break; + + // The comments are considered neither an operator nor an operand. + case Tokens::T_DOC_COMMENT: + case Tokens::T_COMMENT: + break; + + /* + * `new` is considered same as the function call, mainly because + * it's equivalent to the function call. + */ + case Tokens::T_NEW: + break; + + /* + * Like T_IF & co, array(..) needs 3 tokens ("array", "(" and + * ")") for what's essentially just 1 operator. + */ + case Tokens::T_ARRAY: + break; + + /* + * Class::method or $object->method both only count as 1 + * identifier, even though they consist of 3 tokens. + */ + case Tokens::T_OBJECT_OPERATOR: + case Tokens::T_DOUBLE_COLON: + // Glue ->/:: and before & after parts together. + $image = array_pop($operands).$token->image.$tokens[$i + 1]->image; + $operands[] = $image; + + // Skip next part (would be seen as operand) + $skipUntil = $tokens[$i + 1]->type; + break; + + // Ignore HEREDOC delimiters. + case Tokens::T_START_HEREDOC: + case Tokens::T_END_HEREDOC: + break; + + // Ignore PHP open & close tags and non-PHP content. + case Tokens::T_OPEN_TAG: + case Tokens::T_CLOSE_TAG: + case Tokens::T_NO_PHP: + break; + + /* + * The function name is considered a single operator when it + * appears as calling a function, but when it appears in + * declarations or in function definitions it is not counted as + * operator. + * Default parameter assignments are not counted. + */ + case Tokens::T_FUNCTION: + // Because `)` could appear in default argument assignment + // (`$var = array()`), we need to skip until `{`, but that + // one should be included in operators. + $skipUntil = Tokens::T_CURLY_BRACE_OPEN; + $operators[] = '{'; + break; + + /* + * When variables or constants appear in declaration they are + * not considered as operands, they are considered operands only + * when they appear with operators in expressions. + */ + case Tokens::T_VAR: + /* + * Ignore this token, and the next one will be recognized as + * operand but should be cancelled out. + */ + $skipUntil = $tokens[$i + 1]->type; + break; + + case Tokens::T_STRING: + // `define` is T_STRING, just like any other identifier. + if ($token->image === 'define') { + // Undo all of "define", "(", name, ",", value, ")" + $skipUntil = Tokens::T_PARENTHESIS_CLOSE; + } else { + $operands++; + } + break; + + // Operands + case Tokens::T_CONSTANT_ENCAPSED_STRING: + case Tokens::T_VARIABLE: + case Tokens::T_LNUMBER: + case Tokens::T_DNUMBER: + case Tokens::T_NULL: + case Tokens::T_TRUE: + case Tokens::T_FALSE: + case Tokens::T_CLASS_FQN: + case Tokens::T_LINE: + case Tokens::T_METHOD_C: + case Tokens::T_NS_C: + case Tokens::T_DIR: + case TOKENS::T_ENCAPSED_AND_WHITESPACE: // content of HEREDOC + $operands[] = $token->image; + break; + + // Everything else is an operator. + default: + $operators[] = $token->image; + break; + } + } + + $this->metrics[$callable->getId()] = array( + 'n1' => count($operators), + 'n2' => count($operands), + 'N1' => count(array_unique($operators)), + 'N2' => count(array_unique($operands)), + ); + } + + /** + * Calculates Halstead measures from n1, n2, N1 & N2. + * + * @see http://www.verifysoft.com/en_halstead_metrics.html + * @see http://www.grammatech.com/codesonar/workflow-features/halstead + * + * @param array $basis [n1, n2, N1, N2] + * @return array + */ + public function calculateHalsteadMeasures(array $basis) + { + $measures = array(); + $measures[self::M_HALSTEAD_LENGTH] = $basis['N1'] + $basis['N2']; + $measures[self::M_HALSTEAD_VOCABULARY] = $basis['n1'] + $basis['n2']; + $measures[self::M_HALSTEAD_VOLUME] = $measures[self::M_HALSTEAD_LENGTH] * log($measures[self::M_HALSTEAD_VOCABULARY], 2); + $measures[self::M_HALSTEAD_DIFFICULTY] = ($basis['n1'] / 2) * ($basis['N1'] / ($basis['n2'] ?: 1)); + $measures[self::M_HALSTEAD_LEVEL] = 1 / $measures[self::M_HALSTEAD_DIFFICULTY]; + $measures[self::M_HALSTEAD_EFFORT] = $measures[self::M_HALSTEAD_VOLUME] * $measures[self::M_HALSTEAD_DIFFICULTY]; + $measures[self::M_HALSTEAD_TIME] = $measures[self::M_HALSTEAD_EFFORT] / 18; + $measures[self::M_HALSTEAD_BUGS] = pow($measures[self::M_HALSTEAD_EFFORT], (2/3)) / 3000; + $measures[self::M_HALSTEAD_CONTENT] = $measures[self::M_HALSTEAD_VOLUME] / ($measures[self::M_HALSTEAD_DIFFICULTY] ?: 1); + + return $measures; + } +} diff --git a/src/main/php/PDepend/Report/Summary/Xml.php b/src/main/php/PDepend/Report/Summary/Xml.php index c3401171f9..5ee8e288d8 100644 --- a/src/main/php/PDepend/Report/Summary/Xml.php +++ b/src/main/php/PDepend/Report/Summary/Xml.php @@ -144,6 +144,7 @@ public function getAcceptedAnalyzers() 'pdepend.analyzer.coupling', 'pdepend.analyzer.class_level', 'pdepend.analyzer.cohesion', + 'pdepend.analyzer.halstead', ); } diff --git a/src/main/resources/services.xml b/src/main/resources/services.xml index 1b998b2a50..c8653c5ae7 100644 --- a/src/main/resources/services.xml +++ b/src/main/resources/services.xml @@ -107,5 +107,9 @@ + + + +