src/Size.ps1
|
# The size of each unit: lines of code, counted the way SonarQube counts ncloc (#166). # # A line of code is a physical line holding at least one character that is neither whitespace # nor part of a comment. So a line with only a brace counts, every line of a statement continued # over several lines counts, a line holding only a line-continuation backtick counts, and the # non-blank lines inside a here-string count. Blank lines, comment lines and comment-based help do # not. That is SonarQube's definition, word for word, chosen over an invented one: SonarSource was # asked to stop counting brace-only lines and declined, on the ground that one rule across # languages is worth more than a better rule for one, and PowerShell has no definition of its own # -- the one PowerShell tool that reported this per function counted line BREAKS and kept comment # lines, contrary to its own documentation. # # Attributed to the INNERMOST unit, as every other number on a record is: a nested function's # lines are its own and not its parent's, and the script body counts what no function or member # owns. A line holding code of two units -- `} function Next {` -- counts for both. # # Information only. Nothing here gates anything, and nothing here reads anything but tokens and # offsets, so it is pure. function Get-PSCxTokenUnit { # For each token, the key of the innermost unit it belongs to -- or $null for a token that is # not code (a comment, a line break, the end of input). Aligned with -Token by index. # # ONE pass in order, with a stack of the units open at the current token. Unit extents nest -- # a function inside a function, a method inside a class -- so the innermost open unit is the # top of the stack, and anything outside every unit is the script body's. Operators only: a # function call per token would cost ~30us each (#155), so this is one call per FILE. # # Both output types, and the second is the price of the comma-wrap that keeps the array whole: # `, $x` is statically an Object[] wrapper, so declaring only [string[]] fails the analyzer. [OutputType([string[]], [object[]])] [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Token, [Parameter(Mandatory)] [hashtable]$Span ) # Outer before inner when two start at one offset: the outer must be on the stack first. $order = @($Span.Keys | Sort-Object @{ Expression = { $Span[$_].Start } }, @{ Expression = { $Span[$_].End }; Descending = $true }) $owner = [string[]]::new($Token.Count) $stack = [string[]]::new($order.Count) $top = -1 $next = 0 for ($i = 0; $i -lt $Token.Count; $i++) { # Comments, line breaks and the end of input are not code. A line continuation is: its # backtick is a character that is neither whitespace nor comment. if ($Token[$i].Kind -in 'Comment', 'NewLine', 'EndOfInput') { continue } $at = $Token[$i].Extent.StartOffset while ($top -ge 0 -and $Span[$stack[$top]].End -le $at) { $top-- } while ($next -lt $order.Count -and $Span[$order[$next]].Start -le $at) { $stack[++$top] = $order[$next++] } $owner[$i] = ($top -ge 0) ? $stack[$top] : '<script-body>' } return , $owner } function Get-PSCxLinesOfCodeMap { # unit key -> lines of code, for every unit in -Span plus the script body. # # Each unit remembers the last line it counted. Tokens arrive in order, so a line already # counted for a unit is never counted again however many tokens it holds -- and a line holding # code of two units counts once for each. [OutputType([hashtable])] [CmdletBinding()] param( # The parser's tokens, in source order, as Parser::ParseFile returns them. [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Token, # Get-PSCxUnitSpan: unit key -> Start, End offsets. The script body is not in it. [Parameter(Mandatory)] [hashtable]$Span ) $count = @{ '<script-body>' = 0 } $last = @{ '<script-body>' = 0 } foreach ($k in $Span.Keys) { $count[$k] = 0; $last[$k] = 0 } $owner = Get-PSCxTokenUnit -Token $Token -Span $Span for ($i = 0; $i -lt $Token.Count; $i++) { $unit = $owner[$i] if ($null -eq $unit) { continue } # Every token through the same function, one line or several. A shortcut for the one-line # case gave identical output and, measured over three interleaved rounds on a 545 KB file, # was not faster -- so it was a branch with nothing to show for itself. foreach ($line in (Get-PSCxCodeLine -Token $Token[$i])) { if ($line -gt $last[$unit]) { $count[$unit]++; $last[$unit] = $line } } } return $count } function Get-PSCxCodeLine { # The lines of one token that hold a character other than whitespace: its own line for almost # every token, and each non-blank line of one spanning several -- a here-string, a multi-line # string, a continuation. [OutputType([int[]])] [CmdletBinding()] param([Parameter(Mandatory)] $Token) $segments = $Token.Extent.Text -split '\r?\n' $first = $Token.Extent.StartLineNumber return [int[]]@(for ($i = 0; $i -lt $segments.Count; $i++) { if ($segments[$i] -match '\S') { $first + $i } }) } |