Skip to content

Commit

Permalink
Merge pull request #53 from PoshCode/feature/usings
Browse files Browse the repository at this point in the history
Feature/usings
  • Loading branch information
Jaykul committed Feb 15, 2019
2 parents 7cb1c4f + a8cd54b commit 52f3639
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 6 deletions.
82 changes: 82 additions & 0 deletions Source/Private/MoveUsingStatements.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
function MoveUsingStatements {
<#
.SYNOPSIS
A command to comment out and copy to the top of the file the Using Statements
.DESCRIPTION
When all files are merged together, the Using statements from individual files
don't necessarily end up at the beginning of the PSM1, creating Parsing Errors.
This function uses AST to comment out those statements (to preserver line numbering)
and insert them (conserving order) at the top of the script.
Should the merged RootModule already have errors not related to the Using statements,
or no errors caused by misplaced Using statements, this steps is skipped.
If moving (comment & copy) the Using statements introduce parsing errors to the script,
those changes won't be applied to the file.
#>
[CmdletBinding()]
Param(
# Path to the PSM1 file to amend
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)]
$RootModule,

# The encoding defaults to UTF8 (or UTF8NoBom on Core)
[Parameter(DontShow)]
[string]$Encoding = $(if ($IsCoreCLR) { "UTF8NoBom" } else { "UTF8" })
)

$ParseError = $null
$AST = [System.Management.Automation.Language.Parser]::ParseFile(
$RootModule,
[ref]$null,
[ref]$ParseError
)

# Avoid modifying the file if there's no Parsing Error caused by Using Statements or other errors
if (!$ParseError.Where{$_.ErrorId -eq 'UsingMustBeAtStartOfScript'}) {
Write-Debug "No Using Statement Error found."
return
}
# Avoid modifying the file if there's other parsing errors than Using Statements misplaced
if ($ParseError.Where{$_.ErrorId -ne 'UsingMustBeAtStartOfScript'}) {
Write-Warning "Parsing errors found. Skipping Moving Using statements."
return
}

# Find all Using statements including those non erroring (to conserve their order)
$UsingStatementExtents = $AST.FindAll(
{$Args[0] -is [System.Management.Automation.Language.UsingStatementAst]},
$false
).Extent

# Edit the Script content by commenting out existing statements (conserving line numbering)
$ScriptText = $AST.Extent.Text
$InsertedCharOffset = 0
$StatementsToCopy = New-Object System.Collections.ArrayList
foreach ($UsingSatement in $UsingStatementExtents) {
$ScriptText = $ScriptText.Insert($UsingSatement.StartOffset + $InsertedCharOffset, '#')
$InsertedCharOffset++

# Keep track of unique statements we'll need to insert at the top
if (!$StatementsToCopy.Contains($UsingSatement.Text)) {
$null = $StatementsToCopy.Add($UsingSatement.Text)
}
}

$ScriptText = $ScriptText.Insert(0, ($StatementsToCopy -join "`r`n") + "`r`n")

# Verify we haven't introduced new Parsing errors
$null = [System.Management.Automation.Language.Parser]::ParseInput(
$ScriptText,
[ref]$null,
[ref]$ParseError
)

if ($ParseError) {
Write-Warning "Oops, it seems that we introduced parsing error(s) while moving the Using Statements. Cancelling changes."
}
else {
$null = Set-Content -Value $ScriptText -Path $RootModule -Encoding $Encoding
}
}
1 change: 1 addition & 0 deletions Source/Public/Build-Module.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ function Build-Module {
$AllScripts = Get-ChildItem -Path $SourceDirectories.ForEach{ Join-Path $ModuleInfo.ModuleBase $_ } -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue

SetModuleContent -Source (@($ModuleInfo.Prefix) + $AllScripts.FullName + @($ModuleInfo.Suffix)).Where{$_} -Output $RootModule -Encoding "$($ModuleInfo.Encoding)"
MoveUsingStatements -RootModule $RootModule -Encoding "$($ModuleInfo.Encoding)"

# If there is a PublicFilter, update ExportedFunctions
if ($ModuleInfo.PublicFilter) {
Expand Down
139 changes: 139 additions & 0 deletions Tests/Private/MoveUsingStatements.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
Describe "MoveUsingStatements" {

Context "Necessary Parameters" {
$CommandInfo = InModuleScope ModuleBuilder { Get-Command MoveUsingStatements }

It 'has a mandatory RootModule parameter' {
$RootModule = $CommandInfo.Parameters['RootModule']
$RootModule | Should -Not -BeNullOrEmpty
$RootModule.Attributes.Where{$_ -is [Parameter]}.Mandatory | Should -be $true
}

It "has an optional string Encoding parameter" {
$Encoding = $CommandInfo.Parameters['Encoding']
$Encoding | Should -Not -BeNullOrEmpty
$Encoding.ParameterType | Should -Be ([String])
$Encoding.Attributes.Where{$_ -is [Parameter]}.Mandatory | Should -Be $False
}
}

Context "Moving Using Statements to the beginning of the file" {
$MoveUsingStatementsCmd = InModuleScope ModuleBuilder {
Mock Write-Warning {}
Get-Command MoveUsingStatements
}

$TestCases = @(
@{
TestCaseName = '2xUsingMustBeAtStartOfScript Fixed'
PSM1File = "function x {`r`n}`r`n" +
"Using namespace System.io`r`n`r`n" + #UsingMustBeAtStartOfScript
"function y {`r`n}`r`n" +
"Using namespace System.Drawing" #UsingMustBeAtStartOfScript
ErrorBefore = 2
ErrorAfter = 0
},
@{
TestCaseName = 'NoErrors Do Nothing'
PSM1File = "Using namespace System.io`r`n`r`n" +
"Using namespace System.Drawing`r`n" +
"function x { `r`n}`r`n" +
"function y { `r`n}`r`n"
ErrorBefore = 0
ErrorAfter = 0
},
@{
TestCaseName = 'NotValidPowerShel Do Nothing'
PSM1File = "Using namespace System.io`r`n`r`n" +
"function x { `r`n}`r`n" +
"Using namespace System.Drawing`r`n" + # UsingMustBeAtStartOfScript
"function y { `r`n}`r`n}" # Extra } at the end
ErrorBefore = 2
ErrorAfter = 2
}
)

It 'Should succeed test: "<TestCaseName>" from <ErrorBefore> to <ErrorAfter> parsing errors' -TestCases $TestCases {
param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter)

$testModuleFile = "$TestDrive\MyModule.psm1"
Set-Content $testModuleFile -value $PSM1File -Encoding UTF8
# Before
$ErrorFound = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile(
$testModuleFile,
[ref]$null,
[ref]$ErrorFound
)
$ErrorFound.Count | Should -be $ErrorBefore

# After
&$MoveUsingStatementsCmd -RootModule $testModuleFile

$null = [System.Management.Automation.Language.Parser]::ParseFile(
$testModuleFile,
[ref]$null,
[ref]$ErrorFound
)
$ErrorFound.Count | Should -be $ErrorAfter
}
}
Context "When MoveUsingStatements should do nothing" {

$MoveUsingStatementsCmd = InModuleScope ModuleBuilder {
Mock Write-Warning {}
Mock Set-Content {}
Mock Write-Debug {} -ParameterFilter {$Message -eq "No Using Statement Error found." }

Get-Command MoveUsingStatements
}

It 'Should Warn and skip when there are Parsing errors other than Using Statements' {
$testModuleFile = "$TestDrive\MyModule.psm1"
$PSM1File = "Using namespace System.IO`r`n function xyz {}`r`n}`r`nUsing namespace System.Drawing" # extra } Set-Content $testModuleFile -value $PSM1File -Encoding UTF8
Set-Content $testModuleFile -value $PSM1File -Encoding UTF8

&$MoveUsingStatementsCmd -RootModule $testModuleFile
Assert-MockCalled -CommandName Write-Warning -Times 1 -ModuleName ModuleBuilder
Assert-MockCalled -CommandName Set-Content -Times 0 -ModuleName ModuleBuilder
}

It 'Should not do anything when there are no Using Statements Errors' {

$testModuleFile = "$TestDrive\MyModule.psm1"
$PSM1File = "Using namespace System.IO; function x {}"
Set-Content $testModuleFile -value $PSM1File -Encoding UTF8

&$MoveUsingStatementsCmd -RootModule $testModuleFile

Assert-MockCalled -CommandName Write-Debug -Times 1 -ModuleName ModuleBuilder
Assert-MockCalled -CommandName Set-Content -Times 0 -ModuleName ModuleBuilder
(Get-Content -Raw $testModuleFile).Trim() | Should -Be $PSM1File

}


It 'Should not modify file when introducing parsing errors' {

$testModuleFile = "$TestDrive\MyModule.psm1"
$PSM1File = "function x {}`r`nUsing namespace System.IO;"
Set-Content $testModuleFile -value $PSM1File -Encoding UTF8

InModuleScope ModuleBuilder {
Mock New-Object {
# Introducing Parsing Error in the file
$Flag = [System.Collections.ArrayList]::new()
$null = $Flag.Add("MyParsingError}")
Write-Output -NoEnumerate $Flag
}
}

&$MoveUsingStatementsCmd -RootModule $testModuleFile

Assert-MockCalled -CommandName Set-Content -Times 0 -ModuleName ModuleBuilder
Assert-MockCalled -CommandName Write-Warning -Times 1 -ModuleName ModuleBuilder
(Get-Content -Raw $testModuleFile).Trim() | Should -Be $PSM1File

}
}
}
6 changes: 3 additions & 3 deletions Tests/Public/Convert-CodeCoverage.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Describe "Convert-CodeCoverage" {
$ModuleFiles = Get-ChildItem $ModuleRoot -File -Recurse -Filter *.ps1
$ModuleSource = Get-Content $ModulePath

$lineNumber = Get-Random -min 2 -max $ModuleSource.Count
$lineNumber = Get-Random -min 3 -max $ModuleSource.Count
while($ModuleSource[$lineNumber] -match "^#(END)?REGION") {
$lineNumber += 5
}
Expand All @@ -22,14 +22,14 @@ Describe "Convert-CodeCoverage" {
Function = 'CopyReadme'
# these are pipeline bound
File = $ModulePath
Line = 25
Line = 26 # 1 offset with the Using Statement introduced in MoveUsingStatements
}
}
}

$SourceLocation = $PesterResults | Convert-CodeCoverage -SourceRoot $ModuleRoot

$SourceLocation.SourceFile | Should -Be ".\Private\CopyReadme.ps1"
$SourceLocation.Line | Should -Be 24
$SourceLocation.Line | Should -Be 25
}
}
6 changes: 3 additions & 3 deletions Tests/Public/Convert-LineNumber.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Describe "Convert-LineNumber" {
for($i=0; $i -lt 5; $i++) {

# I don't know why I keep trying to do this using random numbers
$lineNumber = Get-Random -min 2 -max $ModuleSource.Count
$lineNumber = Get-Random -min 3 -max $ModuleSource.Count
# but I have to keep avoiding the lines that don't make sense
while($ModuleSource[$lineNumber] -match "^\s*$|^#(END)?REGION|^\s*function\s") {
$lineNumber += 5
Expand Down Expand Up @@ -53,12 +53,12 @@ Describe "Convert-LineNumber" {
Function = 'CopyReadme'
# these are pipeline bound
File = $ModulePath
Line = 25
Line = 26 # 1 offset with the Using Statement introduced in MoveUsingStatements
}

$SourceLocation = $PesterMiss | Convert-LineNumber -Passthru
$SourceLocation.SourceFile | Should -Be ".\Private\CopyReadme.ps1"
$SourceLocation.SourceLineNumber | Should -Be 24
$SourceLocation.SourceLineNumber | Should -Be 25
$SourceLocation.Function | Should -Be 'CopyReadme'
}
}

0 comments on commit 52f3639

Please sign in to comment.