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 @@
+
+
+
+