Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
879bf02
Add AtAtCurly token and allow parsing of it. TODO: return command ele…
bergmeister Jul 6, 2019
600baed
Add Splatted property to hashtable to be able to tell compiler that h…
bergmeister Jul 7, 2019
7460e22
Add tests for existing and new splatting functionality.
bergmeister Jul 7, 2019
bef1af6
restart-ci due to sporadic mac failure
bergmeister Jul 7, 2019
b63df9d
Fix the 2 CodeFactor warning (XML comments of properties must start w…
bergmeister Jul 7, 2019
0c9c50c
Add CI tag to Pester tests to fix build and fix indentation
bergmeister Jul 7, 2019
30a705e
Merge branch 'master' of https://github.com/PowerShell/PowerShell int…
bergmeister Jul 16, 2019
eb75a44
Merge branch 'master' of https://github.com/PowerShell/PowerShell int…
bergmeister Jul 27, 2019
cad3ef5
Convert if statement to switch expression to address first PR comment
bergmeister Jul 27, 2019
44da128
Merge branch 'master' into InlineSplatting_AtAtCurly
bergmeister Aug 26, 2019
c258636
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Sep 1, 2019
1121d86
Perform lookahead in tokenizer to correctly detect @@{ in all cases. …
bergmeister Sep 1, 2019
36c7c88
Make Splatted property readonly but injecting it in the constructor.
bergmeister Sep 1, 2019
a90ea6b
Merge branch 'master' into InlineSplatting_AtAtCurly
Sep 20, 2019
f412369
Add experimental feature PSGeneralizedSplatting. Due to compiler issu…
Sep 21, 2019
b7dbef8
Resolve merge conflict
Sep 21, 2019
1146ef9
undo one accidental local change in last commit
Sep 21, 2019
7cb6496
Use semantic check to restrict usage to command arguments and add mor…
bergmeister Sep 22, 2019
60f89dd
Rename AtAtCurly to AtAt to better represent what it is
bergmeister Sep 22, 2019
9326a0a
Cleanup using statement and whitespace
bergmeister Sep 22, 2019
40c860e
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Oct 3, 2019
7b4a1c7
Do not use lookahead in tokenizer so that the token is @@{
bergmeister Oct 3, 2019
6ac6fb3
Update src/System.Management.Automation/engine/parser/ast.cs
bergmeister Oct 3, 2019
82d8ed8
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Oct 5, 2019
9ec1837
Change AtAtCurly token to At token
bergmeister Oct 5, 2019
4d6870b
Resolve merge conflicts - Merge branch 'master' of http://github.com/…
bergmeister Oct 28, 2019
0416bf6
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Oct 28, 2019
5c78cc3
Reduce lookahead to only @@ to return @ already after having detected @
bergmeister Oct 28, 2019
96f1fae
Do not lookahead for @@. Just return At token at the end.
bergmeister Oct 28, 2019
aa59eb9
Enhance semantic check to require more than 1 element in the hashtabl…
bergmeister Oct 28, 2019
0a81afb
tweak semantic check
bergmeister Oct 29, 2019
3242c9f
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Nov 3, 2019
0020155
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Feb 11, 2020
a8359e3
Merge branch 'master' of https://github.com/PowerShell/PowerShell int…
bergmeister May 3, 2020
9b0c1a9
Merge branch 'master' of http://github.com/powershell/powershell into…
bergmeister Jun 11, 2020
95417ff
Merge branch 'InlineSplatting_AtAtCurly' of http://github.com/bergmei…
bergmeister Jun 11, 2020
61dbd40
Fix merge error in 9b0c1a93be5a7a1ccff4e039137a9165014bc61d
bergmeister Jun 11, 2020
aefbf49
empty commit to rerun ci
bergmeister Jun 12, 2020
c58ab8b
empty commit to rerun ci
bergmeister Jun 17, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: "PSNativePSPathResolution",
description: "Convert PSPath to filesystem path, if possible, for native commands"),
new ExperimentalFeature(
name: "PSGeneralizedSplatting",
description: "Preliminary support for generalized splatting, currently only inline splatting"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

Expand Down
13 changes: 10 additions & 3 deletions src/System.Management.Automation/engine/parser/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4145,10 +4145,17 @@ public object VisitCommand(CommandAst commandAst)
splatTest = usingExpression.SubExpression;
}

VariableExpressionAst variableExpression = splatTest as VariableExpressionAst;
if (variableExpression != null)
switch (splatTest)
{
splatted = variableExpression.Splatted;
case VariableExpressionAst variableExpressionAst:
splatted = variableExpressionAst.Splatted;
break;
case HashtableAst hashTableAst:
if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting"))
{
splatted = hashTableAst.Splatted;
}
break;
}

elementExprs[i] = Expression.Call(
Expand Down
23 changes: 19 additions & 4 deletions src/System.Management.Automation/engine/parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6291,6 +6291,7 @@ private ExpressionAst GetCommandArgument(CommandArgumentContext context, Token t
case TokenKind.AtParen:
case TokenKind.AtCurly:
case TokenKind.LCurly:
case TokenKind.At:
UngetToken(token);
exprAst = PrimaryExpressionRule(withMemberAccess: true);
Diagnostics.Assert(exprAst != null, "PrimaryExpressionRule should never return null");
Expand Down Expand Up @@ -7231,7 +7232,8 @@ private ExpressionAst PrimaryExpressionRule(bool withMemberAccess)
break;

case TokenKind.AtCurly:
expr = HashExpressionRule(token, false /* parsingSchemaElement */ );
case TokenKind.At:
expr = HashExpressionRule(token, parsingSchemaElement: false);
break;

case TokenKind.LCurly:
Expand Down Expand Up @@ -7305,6 +7307,17 @@ private ExpressionAst HashExpressionRule(Token atCurlyToken, bool parsingSchemaE
SkipNewlines();

List<KeyValuePair> keyValuePairs = new List<KeyValuePair>();

bool splatted = false;
if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting"))
{
if (atCurlyToken.Kind == TokenKind.At)
{
NextToken();
splatted = true;
}
}

while (true)
{
KeyValuePair pair = GetKeyValuePair(parsingSchemaElement);
Expand Down Expand Up @@ -7350,9 +7363,11 @@ 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, splatted)
{
IsSchemaElement = parsingSchemaElement,
};
return hashtableAst;
}

private KeyValuePair GetKeyValuePair(bool parsingSchemaElement)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,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.KeyValuePairs.Count > 0 && !(hashtableAst.Parent is CommandAst))
{
_parser.ReportError(hashtableAst.Extent,
nameof(ParserStrings.GeneralizedSplattingOnlyPermittedForCommands),
ParserStrings.GeneralizedSplattingOnlyPermittedForCommands,
hashtableAst);
}
}

HashSet<string> keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in hashtableAst.KeyValuePairs)
{
Expand Down
11 changes: 9 additions & 2 deletions src/System.Management.Automation/engine/parser/ast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9833,11 +9833,12 @@ public class HashtableAst : ExpressionAst
/// </summary>
/// <param name="extent">The extent of the literal, from '@{' to the closing '}'.</param>
/// <param name="keyValuePairs">The optionally null or empty list of key/value pairs.</param>
/// <param name="splatted">Whether it is splatted.</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> is null.
/// </exception>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
public HashtableAst(IScriptExtent extent, IEnumerable<KeyValuePair> keyValuePairs)
public HashtableAst(IScriptExtent extent, IEnumerable<KeyValuePair> keyValuePairs, bool splatted)
: base(extent)
{
if (keyValuePairs != null)
Expand All @@ -9849,6 +9850,7 @@ public HashtableAst(IScriptExtent extent, IEnumerable<KeyValuePair> keyValuePair
{
this.KeyValuePairs = s_emptyKeyValuePairs;
}
Splatted = splatted;
}

/// <summary>
Expand All @@ -9875,7 +9877,7 @@ public override Ast Copy()
}
}

return new HashtableAst(this.Extent, newKeyValuePairs);
return new HashtableAst(this.Extent, newKeyValuePairs, this.Splatted);
}

/// <summary>
Expand All @@ -9886,6 +9888,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; }

/// <summary>
/// Gets or sets a value indicating whether inline splatting syntax (using @) was used, false otherwise.
/// </summary>
public bool Splatted { get; }

#region Visitors

internal override object Accept(ICustomAstVisitor visitor)
Expand Down
20 changes: 14 additions & 6 deletions src/System.Management.Automation/engine/parser/token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -428,6 +429,12 @@ public enum TokenKind
/// <summary>The null conditional index access operator '?[]'.</summary>
QuestionLBracket = 104,

/// <summary>
/// The unary operator used for generalized splatting expressions.
/// This excludes the legacy token <see cref="TokenKind.SplattedVariable"/>.
/// </summary>
At = 105,

#endregion Operators

#region Keywords
Expand Down Expand Up @@ -878,7 +885,7 @@ public static class TokenTraits
/* QuestionQuestion */ TokenFlags.BinaryOperator | TokenFlags.BinaryPrecedenceCoalesce,
/* QuestionDot */ TokenFlags.SpecialOperator | TokenFlags.DisallowedInRestrictedMode,
/* QuestionLBracket */ TokenFlags.None,
/* Reserved slot 7 */ TokenFlags.None,
/* At */ TokenFlags.None,
/* Reserved slot 8 */ TokenFlags.None,
/* Reserved slot 9 */ TokenFlags.None,
/* Reserved slot 10 */ TokenFlags.None,
Expand Down Expand Up @@ -1077,7 +1084,7 @@ public static class TokenTraits
/* QuestionQuestion */ "??",
/* QuestionDot */ "?.",
/* QuestionLBracket */ "?[",
/* Reserved slot 7 */ string.Empty,
/* At */ "@",
/* Reserved slot 8 */ string.Empty,
/* Reserved slot 9 */ string.Empty,
/* Reserved slot 10 */ string.Empty,
Expand Down Expand Up @@ -1154,10 +1161,11 @@ public static class TokenTraits
#if DEBUG
static TokenTraits()
{
Diagnostics.Assert(s_staticTokenFlags.Length == ((int)TokenKind.Default + 1),
"Table size out of sync with enum - _staticTokenFlags");
Diagnostics.Assert(s_tokenText.Length == ((int)TokenKind.Default + 1),
"Table size out of sync with enum - _tokenText");
var maximumEnumValueOfTokenKind = Enum.GetValues(typeof(TokenKind)).Cast<int>().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");
Expand Down
5 changes: 5 additions & 0 deletions src/System.Management.Automation/engine/parser/tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4674,6 +4674,11 @@ internal Token NextToken()
return ScanVariable(true, false);
}

if (ExperimentalFeature.IsEnabled("PSGeneralizedSplatting"))
{
return NewToken(TokenKind.At);
}

ReportError(_currentIndex - 1,
nameof(ParserStrings.UnrecognizedToken),
ParserStrings.UnrecognizedToken);
Expand Down
3 changes: 3 additions & 0 deletions src/System.Management.Automation/resources/ParserStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ Possible matches are</value>
<data name="SplattingNotPermittedInArgumentList" xml:space="preserve">
<value>Splatted variables like '@{0}' cannot be part of a comma-separated list of arguments.</value>
</data>
<data name="GeneralizedSplattingOnlyPermittedForCommands" xml:space="preserve">
<value>'{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.</value>
</data>
<data name="MissingFileSpecification" xml:space="preserve">
<value>Missing file specification after redirection operator.</value>
</data>
Expand Down
33 changes: 33 additions & 0 deletions test/powershell/Language/Parser/Parser.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1349,3 +1349,36 @@ foo``u{2195}abc
}
}
}


Describe 'Splatting' -Tags 'CI' {
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; Verbose = $true } | Should -Not -BeNullOrEmpty
}
}

Context 'Parameter mismatches' {
$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}'" -Skip:$skipTest {
{ Get-Item @@{ ParameterThatDoesNotExist = $tempFile } } | Should -Throw -ErrorId 'NamedParameterNotFound'
}
}
}
76 changes: 76 additions & 0 deletions test/powershell/Language/Parser/Parsing.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,79 @@ Describe "Keywords 'default', 'hidden', 'in', 'static' Token parsing" -Tags CI {
. $Keyword | Should -BeExactly $Keyword
}
}

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]::At;
TokenPosition = 1
}
@{ Script = 'Verb-Noun @@{ "ParameterName1"="ParameterValue1"; "ParameterName2"="ParameterValue2" }';
TokenKind = [System.Management.Automation.Language.TokenKind]::At;
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 <Script> for inline argument splatting of a command" -TestCases $testCases_basic {
param($Script, $TokenKind, [int]$TokenPosition)

$tokens = $null
$errors = $null
$result = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tokens, [ref]$errors)

$tokens[$TokenPosition].Kind | Should -BeExactly $TokenKind
$tokens[$TokenPosition].Text | Should -BeExactly '@'

$result.EndBlock.Statements.PipelineElements[0].CommandElements[1] | Should -BeOfType 'System.Management.Automation.Language.HashtableAst'
$result.EndBlock.Statements.PipelineElements[0].CommandElements[1].Extent.Text.StartsWith('@@{') |
Should -BeTrue -Because "HashtableAst Extent should start with '@@{'"
$result.EndBlock.Statements.PipelineElements[0].CommandElements[1].Extent.Text.EndsWith('}') |
Should -BeTrue -Because "HashtableAst Extent should end with '}'"
}

It "Generalized splatting operator '@@' can be used in function name" {
function a@@ { 'a@@' }
function a@@b { 'a@@b' }

a@@ | Should -BeExactly 'a@@'
a@@b | Should -BeExactly 'a@@b'
}

It "Using generalized splatting expression <Script> not as argument to command should generate correct error" -TestCases $testCases_incomplete {
param($Script, $ErrorId, $AstType)

$errors = $null
[System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$null, [ref]$errors)

$errors.Count | Should -Be 1
$errors.ErrorId | Should -BeExactly $ErrorId
}
}