# encoding: ascii
# api: powershell
# title: Measure-ScriptCode
# description: Measure-ScriptCode calculates some code metrics like the number of lines-of-code, comments, functions from script or module files it is fed. The script is PowerShell 3 or later only because of use of AST (Abstract Syntax Tree).
# version: 0.1
# type: script
# author: Victor Vogelpoel
# license: CC0
# function: Measure-ScriptCode
# x-poshcode-id: 4789
# x-archived: 2014-01-15T19:12:17
# x-published: 2014-01-13T07:22:00
#
# Currently it calculates the following metrics:
# Number of Lines of Code,
# Number of Comments, while multi-line comments are counted as one,
# Number of Functions, filters and workflow functions
# Number of files, module and manifest files
# Total number of Lines, words and characters. 
# The command returns a custom object with the calculated metrics:
# Files                : 189
# Modules              : 8
# Manifests            : 8
# CodeLines            : 11704
# Comments             : 3823
# Functions            : 262
# Workflows            : 8
# Filters              : 1
# Characters           : 559350
# Lines                : 18773
# Words                : 67540
# If you like to learn more, see my blogpost about this command at http://blog.victorvogelpoel.nl/2014/01/12/powershell-measure-scriptcode-calculating-script-code-metrics/
#
# Measure-ScriptCode.ps1
# Return metrics about the script and module files
# PowerShell 3 is required as Abstract Syntax Trees are used.
# Jan 2014
# If this works, Victor Vogelpoel <victor.vogelpoel@macaw.nl> wrote this.
# If it doesn't, I don't know who wrote this.
# Blogpost at http://blog.victorvogelpoel.nl/2014/01/12/powershell-measure-scriptcode-calculating-script-code-metrics/

#requires -version 3
Set-PSDebug -Strict
Set-StrictMode -Version Latest

function Measure-ScriptCode
{
	[CmdletBinding()]
	param
	(
	  [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, HelpMessage="One or more PS1 or PSM1 files to calculate code metrics for")]
	  [Alias('PSPath', 'Path')]
	  [string[]]$ScriptFile
	)
	
	begin
	{
		$files 			= 0
		$modules 		= 0
		$manifests 		= 0
	
		$lines 			= 0
		$words 			= 0
		$characters		= 0
		$codeLines 		= 0
		$comments 		= 0
		$functions		= 0
		$workflows 		= 0
		$filters 		= 0
		$parseErrors	= 0
	}

	process
	{
		foreach ($file in $ScriptFile)
		{
			if ($file -like "*.ps1") { $files++ }
			if ($file -like "*.psm1") { $modules++ }
			if ($file -like "*.psd1") { $manifests++ }
			
			$fileContentsArray	= Get-Content -Path $file
			
			if ($fileContentsArray)
			{
				# First, measure basic metrics
				$measurement 		= $fileContentsArray | Measure-Object -Character -IgnoreWhiteSpace -Word -Line
				$lines 			+= $measurement.Lines
				$words 			+= $measurement.Words
				$characters 		+= $measurement.Characters				
		
				$tokenAst			= $null
				$parseErrorsAst		= $null
				# Use the PowerShell 3 file parser to create the scriptblock AST, tokens and error collections
				$scriptBlockAst		= [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokenAst, [ref]$parseErrorsAst)

				# Get the number of comment lines and comments on the end of a code line
				$comments		+= @($tokenAst | where { $_.Kind -eq "Comment" } ).Length 
				
				# Calculate the 'lines of code': any line not containing comment or commentblock and not an empty or whitespace line.
				# Remove comment tokens from the tokenAst, remove all double newlines and count all the newlines (minus 1)
				$prevTokenIsNewline	= $false
				$codeLines 		+= @($tokenAst | select -ExpandProperty Kind |  where { $_ -ne "comment" } | where {
											if ($_ -ne "NewLine" -or (!$prevTokenIsNewline))
											{
												$_
											}
											$prevTokenIsNewline = ($_ -eq "NewLine")
										} | where { $_ -eq "NewLine" }).Length-1
				
				$parseErrors 		+= @($parseErrorsAst).Length

				if ($scriptBlockAst -ne $null)
				{
					# Find all functions, filters and workflows in the AST, including nested functions
					$functionAst 	= $scriptBlockAst.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true)

					# Count the specific implementation: 'function', 'filter' or 'workflow'
					$functions 	+= @($functionAst | where { (!$_.IsFilter) -and (!$_.IsWorkflow) }).Length
					$filters	+= @($functionAst | where { $_.IsFilter }).Length
					$workflows	+= @($functionAst | where { $_.IsWorkflow }).Length
				}
			}
		}
	}

	end
	{
		return [PSCustomObject]@{
			Files 		= $files
			Modules 	= $modules
			Manifests	= $manifests

			CodeLines	= $codeLines
			Comments 	= $comments
			Functions 	= $functions
			Workflows	= $workflows
			Filters		= $filters

			ParseErrors	= $parseErrors
			Characters 	= $characters
			Lines 		= $lines
			Words		= $words
		}
	}
}
