Skip to content

Fix settings file array parsing when no commas are present #1161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions Engine/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ private static object GetSafeValueFromExpressionAst(ExpressionAst exprAst)
case "false":
return false;

case "null":
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed this was missing and quickly added it. Makes sense from a layering perspective -- null should be parse-able but might not be allowed as a configurable value.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just simplifying the whole code to bool.tryparse ?

Copy link
Contributor Author

@rjmholt rjmholt Mar 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one of the possibilities is null, then it would probably look like:

string varName = varExprAst.VariablePath.UserPath.ToLower();
if (bool.TryParse(varName, out bool val))
{
    return val;
}

if (varName == "null")
{
    return null;
}

throw CreateInvalidDataExceptionFromAst(varExprAst);

I'm not sure that's simpler though -- the switch/case has a nice flow to me, and ideally the C# compiler knows better than I do about how to make it work quickly. (Although it looks like that's not the case, since I would implement a trie for optimum speed).

return null;

default:
throw CreateInvalidDataExceptionFromAst(varExprAst);
}
Expand All @@ -540,24 +543,36 @@ private static object GetSafeValueFromExpressionAst(ExpressionAst exprAst)
return new object[0];
}

PipelineAst pipelineAst = arrExprAst.SubExpression.Statements[0] as PipelineAst;
if (pipelineAst == null)
var listComponents = new List<object>();
// Arrays can either be array expressions (1, 2, 3) or array literals with statements @(1 `n 2 `n 3)
// Or they can be a combination of these
// We go through each statement (line) in an array and read the whole subarray
// This will also mean that @(1; 2) is parsed as an array of two elements, but there's not much point defending against this
foreach (StatementAst statement in arrExprAst.SubExpression.Statements)
{
throw CreateInvalidDataExceptionFromAst(arrExprAst);
}
if (!(statement is PipelineAst pipelineAst))
{
throw CreateInvalidDataExceptionFromAst(arrExprAst);
}

ExpressionAst pipelineExpressionAst = pipelineAst.GetPureExpression();
if (pipelineExpressionAst == null)
{
throw CreateInvalidDataExceptionFromAst(arrExprAst);
ExpressionAst pipelineExpressionAst = pipelineAst.GetPureExpression();
if (pipelineExpressionAst == null)
{
throw CreateInvalidDataExceptionFromAst(arrExprAst);
}

object arrayValue = GetSafeValueFromExpressionAst(pipelineExpressionAst);
// We might hit arrays like @(\n1,2,3\n4,5,6), which the parser sees as two statements containing array expressions
if (arrayValue is object[] subArray)
{
listComponents.AddRange(subArray);
continue;
}

listComponents.Add(arrayValue);
}
return listComponents.ToArray();

// Array expressions may not really be arrays (like `@('a')`, which has no ArrayLiteralAst within)
// However, some rules depend on this always being an array
object arrayValue = GetSafeValueFromExpressionAst(pipelineExpressionAst);
return arrayValue.GetType().IsArray
? arrayValue
: new object[] { arrayValue };

case ArrayLiteralAst arrLiteralAst:
return GetSafeValuesFromArrayAst(arrLiteralAst);
Expand Down Expand Up @@ -647,10 +662,10 @@ private static InvalidDataException CreateInvalidDataExceptionFromAst(Ast ast)
throw new ArgumentNullException(nameof(ast));
}

return ThrowInvalidDataException(ast.Extent);
return CreateInvalidDataException(ast.Extent);
}

private static InvalidDataException ThrowInvalidDataException(IScriptExtent extent)
private static InvalidDataException CreateInvalidDataException(IScriptExtent extent)
{
return new InvalidDataException(string.Format(
CultureInfo.CurrentCulture,
Expand Down
7 changes: 6 additions & 1 deletion Tests/Engine/Settings.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ Describe "Settings Class" {
@{ Expr = '0.142' }
@{ Expr = '"Hello"' }
@{ Expr = '"Well then"' }
@{ Expr = '$null' }
)

$gsvArrayTests = @(
Expand All @@ -218,6 +219,11 @@ Describe "Settings Class" {
@{ Expr = '@("A","B","C")'; Count = 3 }
@{ Expr = '@()'; Count = 0 }
@{ Expr = '@(7)'; Count = 1 }
@{ Expr = "'1',`n'2',`n'3'"; Count = 3 }
@{ Expr = "@(1`n3`n5`n7)"; Count = 4 }
@{ Expr = "'1',`r`n'2',`r`n'3'"; Count = 3 }
@{ Expr = "@(1`r`n3`r`n5`r`n7)"; Count = 4 }
@{ Expr = "@(1,2,3`n7,8,9)"; Count = 6 }
)

$gsvHashtableTests = @(
Expand All @@ -235,7 +241,6 @@ Describe "Settings Class" {
$gsvThrowTests = @(
@{ Expr = '$var' }
@{ Expr = '' }
@{ Expr = '$null' }
@{ Expr = '3+7' }
@{ Expr = '- 2.5'}
@{ Expr = '-not $true' }
Expand Down
133 changes: 133 additions & 0 deletions Tests/Rules/AllCompatibilityRules.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

$script:fileContent = @'
$assetDir = 'C:\ImportantFiles\'
$archiveDir = 'C:\Archived\'

$runningOnWindows = -not ($IsLinux -or $IsMacOS)
$zipCompressionLevel = 'Optimal'
$includeBaseDirInZips = $true

if (-not (Test-Path $archiveDir))
{
New-Item -Type Directory $archiveDir
}

Import-Module -FullyQualifiedName @{ ModuleName = 'MyArchiveUtilities'; ModuleVersion = '2.0' }

$sigs = [System.Collections.Generic.List[System.Management.Automation.Signature]]::new()
$filePattern = [System.Management.Automation.WildcardPattern]::Get('system*')
foreach ($file in Get-ChildItem $assetDir -Recurse -Depth 1)
{
if (-not $filePattern.IsMatch($file.Name))
{
continue
}

if ($file.Name -like '*.dll')
{
$sig = Get-AuthenticodeSignature $file
$sigs.Add($sig)
continue
}

if (Test-WithFunctionFromMyModule -File $file)
{
$destZip = Join-Path $archiveDir $file.BaseName
[System.IO.Compression.ZipFile]::CreateFromDirectory($file.FullName, "$destZip.zip", $zipCompressionLevel, $includeBaseDirInZips)
}
}

Write-Output $sigs
'@

$script:settingsContent = @'
@{
Rules = @{
PSUseCompatibleCommands = @{
Enable = $true
TargetProfiles = @(
'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework' # Server 2019 - PS 5.1 (the platform it already runs on)
'win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework' # Server 2012 - PS 3
'ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core' # Ubuntu 18.04 - PS 6.1
)
}
PSUseCompatibleTypes = @{
Enable = $true
# Same as for command targets
TargetProfiles = @(
'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
'win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework'
'ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core'
)
}
PSUseCompatibleSyntax = @{
Enable = $true
TargetVersions = @('3.0', '5.1', '6.1')
}
}
}
'@

$script:expectedCommandDiagnostics = @(
@{ Command = 'Import-Module'; Parameter = 'FullyQualifiedName'; Line = 13 }
@{ Command = 'Get-ChildItem'; Parameter = 'Depth'; Line = 17 }
@{ Command = 'Get-AuthenticodeSignature'; Line = 26 }
)

$script:expectedTypeDiagnostics = @(
@{ Type = 'System.Management.Automation.WildcardPattern'; Member = 'Get'; Line = 16 }
@{ Type = 'System.IO.Compression.ZipFile'; Line = 34 }
)

$script:expectedSyntaxDiagnostics = @(
@{ Line = 15; CorrectionText = "New-Object 'System.Collections.Generic.List[System.Management.Automation.Signature]'" }
)

Describe "Running all compatibility rules with a settings file" {
BeforeAll {
$testFile = New-Item -Path "$TestDrive/test.ps1" -Value $script:fileContent -ItemType File
$settingsFile = New-Item -Path "$TestDrive/settings.psd1" -Value $script:settingsContent -ItemType File
$diagnostics = Invoke-ScriptAnalyzer -Path $testFile.FullName -Settings $settingsFile.FullName |
Group-Object -Property RuleName -AsHashtable
$commandDiagnostics = $diagnostics.PSUseCompatibleCommands
$typeDiagnostics = $diagnostics.PSUseCompatibleTypes
$syntaxDiagnostics = $diagnostics.PSUseCompatibleSyntax
}

It "Finds the problem with command <Command> on line <Line> in the file" -TestCases $script:expectedCommandDiagnostics {
param([string]$Command, [string]$Parameter, [int]$Line)

$actualDiagnostic = $commandDiagnostics | Where-Object { $_.Command -eq $Command -and $_.Line -eq $Line }
$actualDiagnostic.Command | Should -BeExactly $Command
$actualDiagnostic.Line | Should -Be $Line
if ($Parameter)
{
$actualDiagnostic.Parameter | Should -Be $Parameter
}
}

It "Finds the problem with type <Type> on line <Line> in the file" -TestCases $script:expectedTypeDiagnostics {
param([string]$Type, [string]$Member, [int]$Line)

$actualDiagnostic = $typeDiagnostics | Where-Object { $_.Type -eq $Type -and $_.Line -eq $Line }
$actualDiagnostic.Type | Should -BeExactly $Type
$actualDiagnostic.Line | Should -Be $Line
if ($Member)
{
$actualDiagnostic.Member | Should -Be $Member
}
}

It "Finds the problem with syntax on line <Line> in the file" -TestCases $script:expectedSyntaxDiagnostics {
param([string]$Suggestion, [int]$Line)

$actualDiagnostic = $syntaxDiagnostics | Where-Object { $_.Line -eq $Line }
$actualDiagnostic.Line | Should -Be $Line
if ($Suggestion)
{
$actualDiagnostic.Suggestion | Should -Be $Suggestion
}
}
}