Skip to content

Commit

Permalink
Add analyzer to calculate Halstead measures
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiasmullie committed Apr 11, 2015
1 parent 30b3423 commit 3497862
Show file tree
Hide file tree
Showing 3 changed files with 391 additions and 0 deletions.
386 changes: 386 additions & 0 deletions src/main/php/PDepend/Metrics/Analyzer/HalsteadAnalyzer.php
@@ -0,0 +1,386 @@
<?php

/**
* This file is part of PDepend.
*
* PHP Version 5
*
* Copyright (c) 2015, Matthias Mullie <pdepend@mullie.eu>.
* 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 <b>array</b> with all generated metric values
* for the given <b>$node</b>. If there are no metrics for the requested
* node, this method will return an empty <b>array</b>.
*
* @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;
}
}
1 change: 1 addition & 0 deletions src/main/php/PDepend/Report/Summary/Xml.php
Expand Up @@ -144,6 +144,7 @@ public function getAcceptedAnalyzers()
'pdepend.analyzer.coupling',
'pdepend.analyzer.class_level',
'pdepend.analyzer.cohesion',
'pdepend.analyzer.halstead',
);
}

Expand Down

0 comments on commit 3497862

Please sign in to comment.