From 879bf0216300193110d21e7440d742327db2aa62 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sat, 6 Jul 2019 22:34:58 +0100 Subject: [PATCH 01/24] Add AtAtCurly token and allow parsing of it. TODO: return command elements instead of just hashtable --- .../engine/parser/Parser.cs | 8 +++++++- .../engine/parser/token.cs | 15 +++++++++++---- .../engine/parser/tokenizer.cs | 5 +++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index b48637cd108..7d9b7beb999 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -6080,6 +6080,7 @@ private ExpressionAst GetCommandArgument(CommandArgumentContext context, Token t case TokenKind.AtParen: case TokenKind.AtCurly: case TokenKind.LCurly: + case TokenKind.AtAtCurly: UngetToken(token); exprAst = PrimaryExpressionRule(withMemberAccess: true); Diagnostics.Assert(exprAst != null, "PrimaryExpressionRule should never return null"); @@ -6354,7 +6355,7 @@ internal Ast CommandRule(bool forDynamicKeyword) } else { - var ast = GetCommandArgument(context, token); + var ast = GetCommandArgument(context, token); // chris: return different type of ast or how to get info out, which is an array // If this is the special verbatim argument syntax, look for the next element StringToken argumentToken = token as StringToken; @@ -6859,6 +6860,7 @@ private ExpressionAst PrimaryExpressionRule(bool withMemberAccess) break; case TokenKind.AtCurly: + case TokenKind.AtAtCurly: expr = HashExpressionRule(token, false /* parsingSchemaElement */ ); break; @@ -6933,6 +6935,10 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE SkipNewlines(); List keyValuePairs = new List(); + if (atCurlyToken.Kind == TokenKind.AtAtCurly) + { + NextToken(); // to skip the first '@' to allow the parsing of the hashtable + } while (true) { KeyValuePair pair = GetKeyValuePair(parsingSchemaElement); diff --git a/src/System.Management.Automation/engine/parser/token.cs b/src/System.Management.Automation/engine/parser/token.cs index def6b8d0dc1..ac9d3cd269b 100644 --- a/src/System.Management.Automation/engine/parser/token.cs +++ b/src/System.Management.Automation/engine/parser/token.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Management.Automation.Internal; using System.Text; @@ -570,6 +571,9 @@ public enum TokenKind /// The 'base' keyword Base = 168, + /// The opening token of an inlined splat expression '@@{'. + AtAtCurly = 169, + #endregion Keywords } @@ -921,6 +925,7 @@ public static class TokenTraits /* Command */ TokenFlags.Keyword, /* Hidden */ TokenFlags.Keyword, /* Base */ TokenFlags.Keyword, + /* AtAtCurly */ TokenFlags.None, #endregion Flags for keywords }; @@ -1119,6 +1124,7 @@ public static class TokenTraits /* Command */ "command", /* Hidden */ "hidden", /* Base */ "base", + /* AtAtCurly */ "@@splat", #endregion Text for keywords }; @@ -1126,10 +1132,11 @@ public static class TokenTraits #if DEBUG static TokenTraits() { - Diagnostics.Assert(s_staticTokenFlags.Length == ((int)TokenKind.Base + 1), - "Table size out of sync with enum - _staticTokenFlags"); - Diagnostics.Assert(s_tokenText.Length == ((int)TokenKind.Base + 1), - "Table size out of sync with enum - _tokenText"); + var maximumEnumValueOfTokenKind = Enum.GetValues(typeof(TokenKind)).Cast().Max(); + Diagnostics.Assert(s_staticTokenFlags.Length - 1 == maximumEnumValueOfTokenKind, + $"Table size out of sync with enum - {nameof(s_staticTokenFlags)}"); + Diagnostics.Assert(s_tokenText.Length - 1 == maximumEnumValueOfTokenKind, + $"Table size out of sync with enum - {nameof(s_tokenText)}"); // Some random assertions to make sure the enum and the traits are in sync Diagnostics.Assert(GetTraits(TokenKind.Begin) == (TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName), "Table out of sync with enum - flags Begin"); diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs index 328417a8eae..c46b5e269fa 100644 --- a/src/System.Management.Automation/engine/parser/tokenizer.cs +++ b/src/System.Management.Automation/engine/parser/tokenizer.cs @@ -4613,6 +4613,11 @@ internal Token NextToken() return ScanHereStringExpandable(); } + if (c1 == '@') + { + return NewToken(TokenKind.AtAtCurly); + } + UngetChar(); if (c1.IsVariableStart()) { From 600baed3d8f2bc0e42e038b6ad670f9bd4be75fe Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 7 Jul 2019 14:39:43 +0100 Subject: [PATCH 02/24] Add Splatted property to hashtable to be able to tell compiler that hashtable is an argument. --- .../engine/parser/Compiler.cs | 10 +++++++--- .../engine/parser/Parser.cs | 16 +++++++++++----- .../engine/parser/ast.cs | 5 +++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index a61f36bd292..3d5ca537d94 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -3536,10 +3536,14 @@ public object VisitCommand(CommandAst commandAst) splatTest = usingExpression.SubExpression; } - VariableExpressionAst variableExpression = splatTest as VariableExpressionAst; - if (variableExpression != null) + if (splatTest is VariableExpressionAst variableExpressionAst) { - splatted = variableExpression.Splatted; + splatted = variableExpressionAst.Splatted; + } + + if (splatTest is HashtableAst hashTableAst) + { + splatted = hashTableAst.Splatted; } elementExprs[i] = diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 7d9b7beb999..580577897e7 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -6355,7 +6355,7 @@ internal Ast CommandRule(bool forDynamicKeyword) } else { - var ast = GetCommandArgument(context, token); // chris: return different type of ast or how to get info out, which is an array + var ast = GetCommandArgument(context, token); // If this is the special verbatim argument syntax, look for the next element StringToken argumentToken = token as StringToken; @@ -6861,7 +6861,7 @@ private ExpressionAst PrimaryExpressionRule(bool withMemberAccess) case TokenKind.AtCurly: case TokenKind.AtAtCurly: - expr = HashExpressionRule(token, false /* parsingSchemaElement */ ); + expr = HashExpressionRule(token, parsingSchemaElement: false); break; case TokenKind.LCurly: @@ -6935,9 +6935,12 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE SkipNewlines(); List keyValuePairs = new List(); + + bool splatted = false; if (atCurlyToken.Kind == TokenKind.AtAtCurly) { NextToken(); // to skip the first '@' to allow the parsing of the hashtable + splatted = true; } while (true) { @@ -6984,9 +6987,12 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE endExtent = rCurly.Extent; } - var hashAst = new HashtableAst(ExtentOf(atCurlyToken, endExtent), keyValuePairs); - hashAst.IsSchemaElement = parsingSchemaElement; - return hashAst; + var hashtableAst = new HashtableAst(ExtentOf(atCurlyToken, endExtent), keyValuePairs) + { + IsSchemaElement = parsingSchemaElement, + Splatted = splatted + }; + return hashtableAst; } private KeyValuePair GetKeyValuePair(bool parsingSchemaElement) diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 434b5a6b5e6..adb9d50f4fa 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -9601,6 +9601,11 @@ public override Ast Copy() // Indicates that this ast was constructed as part of a schematized object instead of just a plain hash literal. internal bool IsSchemaElement { get; set; } + /// + /// True if inline splatting syntax (@@) was used, false otherwise. + /// + public bool Splatted { get; set; } + #region Visitors internal override object Accept(ICustomAstVisitor visitor) From 7460e22efd1d9520cd20eb873fd912d60e1e9d8f Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 7 Jul 2019 15:18:56 +0100 Subject: [PATCH 03/24] Add tests for existing and new splatting functionality. --- .../Language/Parser/Parser.Tests.ps1 | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index 3c731fee986..7f7a6d66bed 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -1341,4 +1341,35 @@ foo``u{2195}abc $tokens[1] | Should -BeExactly $lastToken } } + + Describe 'Splatting' { + BeforeAll { + $tempFile = New-TemporaryFile + } + AfterAll { + Remove-Item $tempFile + } + + Context 'Happy Path' { + It "Splatting using hashtable variable '@var'" { + $splattedHashTable = @{ Path = $tempFile } + Get-Item @splattedHashTable | Should -Not -BeNullOrEmpty + } + + It "Splatting using inlined hashtable '@@{key=value}'" { + Get-Item @@{ Path = $tempFile } | Should -Not -BeNullOrEmpty + } + } + + Context 'Parameter mismatches' { + It "Splatting using hashtable variable '@var'" { + $splattedHashTable = @{ ParameterThatDoesNotExist = $tempFile } + { Get-Item @splattedHashTable } | Should -Throw -ErrorId 'NamedParameterNotFound' + } + + It "Splatting using inlined hashtable '@@{key=value}'" { + { Get-Item @@{ ParameterThatDoesNotExist = $tempFile } } | Should -Throw -ErrorId 'NamedParameterNotFound' + } + } + } } From bef1af6aee600b61caa23a3de3915e85cb3951c5 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 7 Jul 2019 15:26:10 +0100 Subject: [PATCH 04/24] restart-ci due to sporadic mac failure From b63df9ddafadfd7c186696e9a90abc937b9ef3fd Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 7 Jul 2019 15:31:30 +0100 Subject: [PATCH 05/24] Fix the 2 CodeFactor warning (XML comments of properties must start with 'Gets or sets a value indicating whether' and a missing newline after a closing brace). --- src/System.Management.Automation/engine/parser/Parser.cs | 1 + src/System.Management.Automation/engine/parser/ast.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 580577897e7..545fff2da2e 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -6942,6 +6942,7 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE NextToken(); // to skip the first '@' to allow the parsing of the hashtable splatted = true; } + while (true) { KeyValuePair pair = GetKeyValuePair(parsingSchemaElement); diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index adb9d50f4fa..a37195abc89 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -9602,7 +9602,7 @@ public override Ast Copy() internal bool IsSchemaElement { get; set; } /// - /// True if inline splatting syntax (@@) was used, false otherwise. + /// Gets or sets a value indicating whether inline splatting syntax (@@) was used, false otherwise. /// public bool Splatted { get; set; } From 0c9c50c91e72b706df6b97fd9b1b25dc6fc907e3 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 7 Jul 2019 15:36:32 +0100 Subject: [PATCH 06/24] Add CI tag to Pester tests to fix build and fix indentation --- .../Language/Parser/Parser.Tests.ps1 | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index 7f7a6d66bed..b038b19983f 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -1341,35 +1341,36 @@ foo``u{2195}abc $tokens[1] | Should -BeExactly $lastToken } } +} - Describe 'Splatting' { - BeforeAll { - $tempFile = New-TemporaryFile - } - AfterAll { - Remove-Item $tempFile - } - Context 'Happy Path' { - It "Splatting using hashtable variable '@var'" { - $splattedHashTable = @{ Path = $tempFile } - Get-Item @splattedHashTable | Should -Not -BeNullOrEmpty - } +Describe 'Splatting' -Tags 'CI' { + BeforeAll { + $tempFile = New-TemporaryFile + } + AfterAll { + Remove-Item $tempFile + } - It "Splatting using inlined hashtable '@@{key=value}'" { - Get-Item @@{ Path = $tempFile } | Should -Not -BeNullOrEmpty - } + Context 'Happy Path' { + It "Splatting using hashtable variable '@var'" { + $splattedHashTable = @{ Path = $tempFile } + Get-Item @splattedHashTable | Should -Not -BeNullOrEmpty } - Context 'Parameter mismatches' { - It "Splatting using hashtable variable '@var'" { - $splattedHashTable = @{ ParameterThatDoesNotExist = $tempFile } - { Get-Item @splattedHashTable } | Should -Throw -ErrorId 'NamedParameterNotFound' - } + It "Splatting using inlined hashtable '@@{key=value}'" { + Get-Item @@{ Path = $tempFile } | Should -Not -BeNullOrEmpty + } + } - It "Splatting using inlined hashtable '@@{key=value}'" { - { Get-Item @@{ ParameterThatDoesNotExist = $tempFile } } | Should -Throw -ErrorId 'NamedParameterNotFound' - } + Context 'Parameter mismatches' { + It "Splatting using hashtable variable '@var'" { + $splattedHashTable = @{ ParameterThatDoesNotExist = $tempFile } + { Get-Item @splattedHashTable } | Should -Throw -ErrorId 'NamedParameterNotFound' + } + + It "Splatting using inlined hashtable '@@{key=value}'" { + { Get-Item @@{ ParameterThatDoesNotExist = $tempFile } } | Should -Throw -ErrorId 'NamedParameterNotFound' } } } From cad3ef52b974e7605acb9847bc56307d346f7cd9 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sat, 27 Jul 2019 16:15:15 +0100 Subject: [PATCH 07/24] Convert if statement to switch expression to address first PR comment --- .../engine/parser/Compiler.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 3d5ca537d94..e156791fdcb 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -3536,14 +3536,14 @@ public object VisitCommand(CommandAst commandAst) splatTest = usingExpression.SubExpression; } - if (splatTest is VariableExpressionAst variableExpressionAst) + switch (splatTest) { - splatted = variableExpressionAst.Splatted; - } - - if (splatTest is HashtableAst hashTableAst) - { - splatted = hashTableAst.Splatted; + case VariableExpressionAst variableExpressionAst: + splatted = variableExpressionAst.Splatted; + break; + case HashtableAst hashTableAst: + splatted = hashTableAst.Splatted; + break; } elementExprs[i] = From 1121d861cb738af0a6f86887d0a8e8fdb7b0e641 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 1 Sep 2019 22:17:30 +0100 Subject: [PATCH 08/24] Perform lookahead in tokenizer to correctly detect @@{ in all cases. The following still produces a hashtable though (it should probably throw an error, which the RTM version does atm): @@{'a'='b'} --- src/System.Management.Automation/engine/parser/tokenizer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs index c46b5e269fa..0bcbb388459 100644 --- a/src/System.Management.Automation/engine/parser/tokenizer.cs +++ b/src/System.Management.Automation/engine/parser/tokenizer.cs @@ -4615,7 +4615,11 @@ internal Token NextToken() if (c1 == '@') { - return NewToken(TokenKind.AtAtCurly); + var c2 = PeekChar(); + if (c2 == '{') + { + return NewToken(TokenKind.AtAtCurly); + } } UngetChar(); From 36c7c888b3fb74891966365cbfbab34991b31e08 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 1 Sep 2019 22:33:19 +0100 Subject: [PATCH 09/24] Make Splatted property readonly but injecting it in the constructor. --- src/System.Management.Automation/engine/parser/Parser.cs | 3 +-- src/System.Management.Automation/engine/parser/ast.cs | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 545fff2da2e..0468fd3e3e9 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -6988,10 +6988,9 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE endExtent = rCurly.Extent; } - var hashtableAst = new HashtableAst(ExtentOf(atCurlyToken, endExtent), keyValuePairs) + var hashtableAst = new HashtableAst(ExtentOf(atCurlyToken, endExtent), keyValuePairs, splatted) { IsSchemaElement = parsingSchemaElement, - Splatted = splatted }; return hashtableAst; } diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index a37195abc89..ab512368bcc 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -9548,11 +9548,12 @@ public class HashtableAst : ExpressionAst /// /// The extent of the literal, from '@{' to the closing '}'. /// The optionally null or empty list of key/value pairs. + /// Whether it is splatted. /// /// If is null. /// [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] - public HashtableAst(IScriptExtent extent, IEnumerable keyValuePairs) + public HashtableAst(IScriptExtent extent, IEnumerable keyValuePairs, bool splatted) : base(extent) { if (keyValuePairs != null) @@ -9564,6 +9565,7 @@ public HashtableAst(IScriptExtent extent, IEnumerable keyValuePair { this.KeyValuePairs = s_emptyKeyValuePairs; } + Splatted = splatted; } /// @@ -9590,7 +9592,7 @@ public override Ast Copy() } } - return new HashtableAst(this.Extent, newKeyValuePairs); + return new HashtableAst(this.Extent, newKeyValuePairs, this.Splatted); } /// @@ -9604,7 +9606,7 @@ public override Ast Copy() /// /// Gets or sets a value indicating whether inline splatting syntax (@@) was used, false otherwise. /// - public bool Splatted { get; set; } + public bool Splatted { get; } #region Visitors From f412369f76139538e1a5010b8891e3686155c413 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sat, 21 Sep 2019 17:11:31 +0100 Subject: [PATCH 10/24] Add experimental feature PSGeneralizedSplatting. Due to compiler issues, some switch statements around AtAtCurly could not be guarded but that is OK since the tokeniser will never produce AtAtCurly if the experimental feature isn't enabled --- .../engine/ExperimentalFeature/ExperimentalFeature.cs | 5 ++++- .../engine/parser/Compiler.cs | 5 ++++- .../engine/parser/Parser.cs | 9 ++++++--- .../engine/parser/tokenizer.cs | 11 +++++++---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 3559477ce5d..fa8e05e19ff 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -114,7 +114,10 @@ static ExperimentalFeature() description: "New parameter set for ForEach-Object to run script blocks in parallel"), new ExperimentalFeature( name: "PSTernaryOperator", - description: "Support the ternary operator in PowerShell langauge.") + description: "Support the ternary operator in PowerShell langauge."), + new ExperimentalFeature( + name: "PSGeneralizedSplatting", + description: "Preliminary support for generalized splatting, currently only inline splatting."), }; EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index a84a3170206..bf2424c42e8 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -3542,7 +3542,10 @@ public object VisitCommand(CommandAst commandAst) splatted = variableExpressionAst.Splatted; break; case HashtableAst hashTableAst: - splatted = hashTableAst.Splatted; + if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting")) + { + splatted = hashTableAst.Splatted; + } break; } diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 35d1b6c9c1e..c650ef06aca 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -7088,10 +7088,13 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE List keyValuePairs = new List(); bool splatted = false; - if (atCurlyToken.Kind == TokenKind.AtAtCurly) + if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting")) { - NextToken(); // to skip the first '@' to allow the parsing of the hashtable - splatted = true; + if (atCurlyToken.Kind == TokenKind.AtAtCurly) + { + NextToken(); // to skip the first '@' to allow the parsing of the hashtable + splatted = true; + } } while (true) diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs index 837bc46a88c..0ff536732dd 100644 --- a/src/System.Management.Automation/engine/parser/tokenizer.cs +++ b/src/System.Management.Automation/engine/parser/tokenizer.cs @@ -4622,12 +4622,15 @@ internal Token NextToken() return ScanHereStringExpandable(); } - if (c1 == '@') + if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting")) { - var c2 = PeekChar(); - if (c2 == '{') + if (c1 == '@') { - return NewToken(TokenKind.AtAtCurly); + var c2 = PeekChar(); + if (c2 == '{') + { + return NewToken(TokenKind.AtAtCurly); + } } } From 1146ef9ec3cf79724b00f700513a71a64d5fdd0d Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sat, 21 Sep 2019 19:48:15 +0100 Subject: [PATCH 11/24] undo one accidental local change in last commit --- src/System.Management.Automation/engine/parser/Compiler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 0d3e11d4ab0..bf2424c42e8 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -3542,7 +3542,6 @@ public object VisitCommand(CommandAst commandAst) splatted = variableExpressionAst.Splatted; break; case HashtableAst hashTableAst: - var type = splatTest.Parent.GetType(); if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting")) { splatted = hashTableAst.Splatted; From 7cb6496ca05d5f78f848735c4a21b47343713966 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 22 Sep 2019 16:51:44 +0100 Subject: [PATCH 12/24] Use semantic check to restrict usage to command arguments and add more test cases. TOOD: Rename AtAtCurly to AtAt --- .../engine/parser/SemanticChecks.cs | 12 +++ .../resources/ParserStrings.resx | 3 + .../Language/Parser/Parser.Tests.ps1 | 7 +- .../Language/Parser/Parsing.Tests.ps1 | 76 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/SemanticChecks.cs b/src/System.Management.Automation/engine/parser/SemanticChecks.cs index 66a6e4f8054..1f138d77ff6 100644 --- a/src/System.Management.Automation/engine/parser/SemanticChecks.cs +++ b/src/System.Management.Automation/engine/parser/SemanticChecks.cs @@ -1085,6 +1085,18 @@ public override AstVisitAction VisitVariableExpression(VariableExpressionAst var public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) { + if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting")) + { + // Check usage of generalized splatting, which supports only arguments to a command at the moment + if (hashtableAst.Splatted && !(hashtableAst.Parent is CommandAst)) + { + _parser.ReportError(hashtableAst.Extent, + nameof(ParserStrings.GeneralizedSplattingOnlyPermittedForCommands), + ParserStrings.GeneralizedSplattingOnlyPermittedForCommands, + hashtableAst); + } + } + HashSet keys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var entry in hashtableAst.KeyValuePairs) { diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index 7d68e49d66b..e558249afba 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -212,6 +212,9 @@ Possible matches are Splatted variables like '@{0}' cannot be part of a comma-separated list of arguments. + + '{0}' uses the generalized splatting operator, which can be used only as an inline argument to a command. Declare a hashtable variable instead if the intent is to splat it to a command at a later time. + Missing file specification after redirection operator. diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index b038b19983f..784c3e0e430 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -1359,17 +1359,18 @@ Describe 'Splatting' -Tags 'CI' { } It "Splatting using inlined hashtable '@@{key=value}'" { - Get-Item @@{ Path = $tempFile } | Should -Not -BeNullOrEmpty + Get-Item @@{ Path = $tempFile; Verbose = $true } | Should -Not -BeNullOrEmpty } } Context 'Parameter mismatches' { - It "Splatting using hashtable variable '@var'" { + $skipTest = -not $EnabledExperimentalFeatures.Contains('PSGeneralizedSplatting') + It "Splatting using hashtable variable '@var'" -Skip:$skipTest { $splattedHashTable = @{ ParameterThatDoesNotExist = $tempFile } { Get-Item @splattedHashTable } | Should -Throw -ErrorId 'NamedParameterNotFound' } - It "Splatting using inlined hashtable '@@{key=value}'" { + It "Splatting using inlined hashtable '@@{key=value}'" -Skip:$skipTest { { Get-Item @@{ ParameterThatDoesNotExist = $tempFile } } | Should -Throw -ErrorId 'NamedParameterNotFound' } } diff --git a/test/powershell/Language/Parser/Parsing.Tests.ps1 b/test/powershell/Language/Parser/Parsing.Tests.ps1 index bf07d5914b8..f1860e2fd3b 100644 --- a/test/powershell/Language/Parser/Parsing.Tests.ps1 +++ b/test/powershell/Language/Parser/Parsing.Tests.ps1 @@ -423,3 +423,79 @@ Describe "Ternary Operator parsing" -Tags CI { $expr.IfFalse | Should -BeOfType 'System.Management.Automation.Language.ConstantExpressionAst' } } + +Describe "Generalized Splatting - Parsing" -Tags CI { + BeforeAll { + $skipTest = -not $EnabledExperimentalFeatures.Contains('PSGeneralizedSplatting') + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSGeneralizedSplatting' to be enabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } + else { + $testCases_basic = @( + @{ Script = 'Verb-Noun @@{ "ParameterName"="ParameterValue" }'; + TokenKind = [System.Management.Automation.Language.TokenKind]::AtAtCurly; + TokenPosition = 1 + } + @{ Script = 'Verb-Noun @@{ "ParameterName1"="ParameterValue1"; "ParameterName2"="ParameterValue2" }'; + TokenKind = [System.Management.Automation.Language.TokenKind]::AtAtCurly; + TokenPosition = 1 + } + ) + + $testCases_incomplete = @( + @{ Script = '@@{ "Key"="Value" }'; + ErrorId = "GeneralizedSplattingOnlyPermittedForCommands"; + AstType = [System.Management.Automation.Language.ErrorExpressionAst] + } + # The following test case is incomplete at the moment but could be implemented as per RFC0002 + @{ Script = '$str="1234"; $str.SubString(@@{ StartIndex = 2; Length = 2 })'; + ErrorId = "GeneralizedSplattingOnlyPermittedForCommands"; + AstType = [System.Management.Automation.Language.ErrorExpressionAst] + } + ) + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } + } + + It "Using generalized splatting operator '@@' in script