├── module
├── PSLambda.types.ps1xml
├── PSLambda.psm1
├── PSLambda.format.ps1xml
└── PSLambda.psd1
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── debugHarness.ps1
├── src
└── PSLambda
│ ├── .vscode
│ └── launch.json
│ ├── PSLambda.csproj
│ ├── LoopScope.cs
│ ├── Commands
│ ├── ICommandHandler.cs
│ ├── WithCommand.cs
│ ├── LockCommand.cs
│ ├── CommandService.cs
│ ├── DefaultCommand.cs
│ ├── ObjectAndBodyCommandHandler.cs
│ └── GenericCommand.cs
│ ├── ScopeHandle.cs
│ ├── stylecop.json
│ ├── IParseErrorWriter.cs
│ ├── LoopScopeStack.cs
│ ├── PSVariableWrapper.cs
│ ├── VariableUtils.cs
│ ├── Empty.cs
│ ├── LabelScope.cs
│ ├── PSLambdaAssemblyInitializer.cs
│ ├── PSLambda.ruleset
│ ├── ExpressionExtensions.cs
│ ├── NewPSDelegateCommand.cs
│ ├── Strings.cs
│ ├── LabelScopeStack.cs
│ ├── DelegateTypeConverter.cs
│ ├── SpecialVariables.cs
│ ├── PSDelegate.cs
│ ├── ParseErrorWriter.cs
│ ├── ParseWriterExtensions.cs
│ ├── resources
│ ├── CompilerStrings.resx
│ └── ErrorStrings.resx
│ ├── VariableScopeStack.cs
│ ├── VariableScope.cs
│ ├── ReflectionCache.cs
│ └── DelegateSyntaxVisitor.cs
├── .circleci
└── config.yml
├── test
├── PSLambda.Tests.ps1
├── ParseError.Tests.ps1
├── Scoping.Tests.ps1
├── PSDelegate.Tests.ps1
├── Commands.Tests.ps1
├── IfStatements.Tests.ps1
├── MethodResolution.Tests.ps1
├── TryCatchFinally.Tests.ps1
├── Loops.Tests.ps1
└── MiscLanguageFeatures.Tests.ps1
├── tools
├── InvokeCircleCI.ps1
├── AssertRequiredModule.ps1
├── GetOpenCover.ps1
└── GetDotNet.ps1
├── LICENSE
├── appveyor.yml
├── ScriptAnalyzerSettings.psd1
├── PSLambda.sln
├── .gitattributes
├── docs
├── CODE_OF_CONDUCT.md
└── CONTRIBUTING.md
├── .gitignore
└── PSLambda.build.ps1
/module/PSLambda.types.ps1xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PSLambda.PSDelegate
5 |
6 | PSLambda.DelegateTypeConverter
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "ms-vscode.csharp",
6 | "ms-vscode.powershell",
7 | "DavidAnson.vscode-markdownlint",
8 | ],
9 | }
10 |
--------------------------------------------------------------------------------
/module/PSLambda.psm1:
--------------------------------------------------------------------------------
1 | Import-Module $PSScriptRoot\PSLambda.dll
2 |
3 | # Can't be in psd1 as TypesToProcess loads before the assembly and it tries to resolve the
4 | # TypeConverter that isn't in the AppDomain yet.
5 | Update-TypeData -AppendPath $PSScriptRoot\PSLambda.types.ps1xml
6 |
7 | Export-ModuleMember -Cmdlet New-PSDelegate
8 |
--------------------------------------------------------------------------------
/debugHarness.ps1:
--------------------------------------------------------------------------------
1 | # Use this file to debug the module.
2 | Import-Module -Name $PSScriptRoot\Release\PSLambda\*\PSLambda.psd1
3 |
4 | $delegate = New-PSDelegate {
5 | $didFail = $false
6 | try {
7 | throw
8 | } catch [InvalidOperationException] {
9 | $didFail = $true
10 | }
11 |
12 | return $didFail
13 | }
14 |
--------------------------------------------------------------------------------
/src/PSLambda/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Attach",
9 | "type": "clr",
10 | "request": "attach",
11 | "processId": "${command:pickProcess}"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.0
2 | jobs:
3 | build:
4 | working_directory: ~/pslambda
5 | docker:
6 | - image: microsoft/powershell@sha256:c0bd0f7ad40bcc76130cb5e772515546e3d200f8a416a273b2605a942d9775f5
7 | steps:
8 | - checkout
9 | - run:
10 | name: Run build script
11 | command: 'pwsh -File ./tools/InvokeCircleCI.ps1'
12 | - store_artifacts:
13 | path: ./testresults/pester.xml
14 | prefix: tests
15 |
--------------------------------------------------------------------------------
/test/PSLambda.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Describe 'module manifest values' {
5 | It 'can retrieve manfiest data' {
6 | $script:manifest = Test-ModuleManifest $manifestPath
7 | }
8 | It 'has the correct name' {
9 | $script:manifest.Name | Should Be $moduleName
10 | }
11 | It 'has the correct guid' {
12 | $script:manifest.Guid | Should Be '242ef850-1f6d-4647-acbe-26d010c4a3f5'
13 | }
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/tools/InvokeCircleCI.ps1:
--------------------------------------------------------------------------------
1 | $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 'true'
2 | $env:DOTNET_CLI_TELEMETRY_OPTOUT = 'true'
3 |
4 | & "$PSScriptRoot/../build.ps1" -Force
5 | Invoke-Build -File $PSScriptRoot/../PSLambda.build.ps1 -Configuration Release -Task Prerelease
6 |
7 | $resultsFile = "$PSScriptRoot/../testresults/pester.xml"
8 |
9 | $passed = (Test-Path $resultsFile) -and 0 -eq ([int]([xml](Get-Content $resultsFile -Raw)).'test-results'.failures)
10 |
11 | if (-not $passed) {
12 | $Error | Format-List * -Force
13 | exit 1
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": ".NET FullCLR Attach",
6 | "type": "clr",
7 | "request": "attach",
8 | "processId": "${command:pickProcess}",
9 | "justMyCode": true,
10 | },
11 | {
12 | "name": ".NET CoreCLR Attach",
13 | "type": "coreclr",
14 | "request": "attach",
15 | "processId": "${command:pickProcess}",
16 | "justMyCode": true,
17 | },
18 | ],
19 | }
20 |
--------------------------------------------------------------------------------
/module/PSLambda.format.ps1xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PSLambda.PSDelegate
6 |
7 | PSLambda.PSDelegate
8 |
9 |
10 |
11 |
12 |
13 | ScriptBlock
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/PSLambda/PSLambda.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | true
5 | PSLambda.ruleset
6 | true
7 | preview
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/PSLambda/LoopScope.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 |
3 | namespace PSLambda
4 | {
5 | ///
6 | /// Represents a scope in which the break or continue keywords
7 | /// may be used.
8 | ///
9 | internal class LoopScope
10 | {
11 | ///
12 | /// Gets or sets the parent scope.
13 | ///
14 | public LoopScope Parent { get; set; }
15 |
16 | ///
17 | /// Gets or sets the label for the break keyword.
18 | ///
19 | public LabelTarget Break { get; set; }
20 |
21 | ///
22 | /// Gets or sets the label for the continue keyword.
23 | ///
24 | public LabelTarget Continue { get; set; }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | //-------- Files configuration --------
3 |
4 | // When enabled, will trim trailing whitespace when you save a file.
5 | "files.trimTrailingWhitespace": true,
6 |
7 | // When enabled, insert a final new line at the end of the file when saving it.
8 | "files.insertFinalNewline": true,
9 |
10 | // Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting.
11 | "search.exclude": {
12 | "Release": true,
13 | "tools/dotnet": true,
14 | "tools/opencover": true,
15 | },
16 |
17 | //-------- PowerShell Configuration --------
18 |
19 | // Use a custom PowerShell Script Analyzer settings file for this workspace.
20 | // Relative paths for this setting are always relative to the workspace root dir.
21 | "powershell.scriptAnalysis.settingsPath": "ScriptAnalyzerSettings.psd1",
22 | }
23 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/ICommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using System.Management.Automation.Language;
3 |
4 | namespace PSLambda.Commands
5 | {
6 | ///
7 | /// Provides handling for a custom command.
8 | ///
9 | internal interface ICommandHandler
10 | {
11 | ///
12 | /// Gets the name of the command.
13 | ///
14 | string CommandName { get; }
15 |
16 | ///
17 | /// Creates a Linq expression for a representing
18 | /// the custom command.
19 | ///
20 | /// The AST to convert.
21 | /// The requesting the expression.
22 | /// An expression representing the command.
23 | Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tools/AssertRequiredModule.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [Parameter(Mandatory)]
4 | [ValidateNotNullOrEmpty()]
5 | [string] $Name,
6 |
7 | [Parameter(Mandatory)]
8 | [version] $RequiredVersion,
9 |
10 | [Parameter()]
11 | [ValidateSet('CurrentUser', 'AllUsers')]
12 | [string] $Scope = 'CurrentUser',
13 |
14 | [Parameter()]
15 | [switch] $Force
16 | )
17 | end {
18 | if (Get-Module $Name -ErrorAction Ignore) {
19 | Remove-Module $Name -Force
20 | }
21 |
22 | $importModuleSplat = @{
23 | RequiredVersion = $RequiredVersion
24 | Name = $Name
25 | ErrorAction = 'Stop'
26 | }
27 |
28 | # TODO: Install required versions into the tools folder
29 | try {
30 | Import-Module @importModuleSplat -Force
31 | } catch [System.IO.FileNotFoundException] {
32 | Install-Module @importModuleSplat -Force:$Force.IsPresent -Scope $Scope
33 | Import-Module @importModuleSplat -Force
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/PSLambda/ScopeHandle.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace PSLambda
4 | {
5 | ///
6 | /// Represents a handle for the current scope.
7 | ///
8 | internal class ScopeHandle : IDisposable
9 | {
10 | private readonly Action _disposer;
11 |
12 | private bool _isDisposed;
13 |
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | /// The action that disposes of the scope.
18 | public ScopeHandle(Action disposer)
19 | {
20 | _disposer = disposer;
21 | }
22 |
23 | ///
24 | /// Disposes of the current scope.
25 | ///
26 | public void Dispose()
27 | {
28 | if (_isDisposed)
29 | {
30 | return;
31 | }
32 |
33 | _disposer?.Invoke();
34 | _isDisposed = true;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/PSLambda/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "indentation": {
5 | "indentationSize": 4,
6 | "tabSize": 4,
7 | "useTabs": false
8 | },
9 | "orderingRules": {
10 | "elementOrder": [
11 | "kind",
12 | "accessibility",
13 | "constant",
14 | "static",
15 | "readonly"
16 | ],
17 | "systemUsingDirectivesFirst": true,
18 | "usingDirectivesPlacement": "outsideNamespace",
19 | "blankLinesBetweenUsingGroups": "allow"
20 | },
21 | "layoutRules": {
22 | "newlineAtEndOfFile": "require"
23 | },
24 | "documentationRules": {
25 | "xmlHeader": false,
26 | "documentationCulture": "en-US",
27 | "documentExposedElements": true,
28 | "documentInternalElements": true
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Patrick M. Meinecke
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/test/ParseError.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Import-Module $manifestPath -Force
5 |
6 | Describe 'parse error tests' {
7 | It 'unsupported AST type throws a parse exception' -TestCases (
8 | @{ Ast = 'FunctionDefinitionAst'; Test = { function Test {} }},
9 | @{ Ast = 'TypeDefinitionAst'; Test = { class Test {} }},
10 | @{ Ast = 'CommandAst'; Test = { Get-ChildItem }},
11 | @{ Ast = 'DataStatementAst'; Test = { data {} }},
12 | @{ Ast = 'FileRedirectionAst'; Test = { $ExecutionContext > 'test' }},
13 | @{ Ast = 'MergingRedirectionAst'; Test = { $ExecutionContext 2>&1 }},
14 | @{ Ast = 'TrapStatementAst'; Test = { trap {} end{ $ExecutionContext }}},
15 | @{ Ast = 'AttributeAst'; Test = { [Parameter()]$thing = $null }},
16 | @{ Ast = 'UsingExpressionAst'; Test = { $using:ExecutionContext }}
17 | ) -Test {
18 | param($Test)
19 | end {
20 | { New-PSDelegate $Test } | Should -Throw 'Unable to compile ScriptBlock due to an unsupported element'
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/Scoping.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Import-Module $manifestPath -Force
5 |
6 | Describe 'variable scoping tests' {
7 | It 'can access current scope variables' {
8 | & {
9 | $v = 'test'
10 | $delegate = New-PSDelegate { $v }
11 | $delegate.Invoke() | Should -Be test
12 | }
13 | }
14 |
15 | It 'can change current scope variables' {
16 | & {
17 | $v = 'test'
18 | $delegate = New-PSDelegate { $v = 'newvalue' }
19 | $delegate.Invoke()
20 | $v | Should -Be newvalue
21 | }
22 | }
23 |
24 | It 'can not access variables outside of the current scope' {
25 | $v = 'test'
26 | & {
27 | { (New-PSDelegate { $v }).Invoke()} | Should -Throw 'The variable "v" was referenced before'
28 | }
29 | }
30 |
31 | It 'can access all scope variables' {
32 | $delegate = New-PSDelegate { $ExecutionContext }
33 | $delegate.Invoke() | Should -BeOfType System.Management.Automation.EngineIntrinsics
34 | }
35 |
36 | It 'property types "strongly typed" PowerShell variables' {
37 | [int] $definitelyNewVar = 0
38 | $delegate = New-PSDelegate { $definitelyNewVar + 1 }
39 | $delegate.Invoke() | Should -Be 1
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/PSLambda/IParseErrorWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Management.Automation.Language;
2 |
3 | namespace PSLambda
4 | {
5 | ///
6 | /// Provides uniform writing and management of objects.
7 | ///
8 | internal interface IParseErrorWriter
9 | {
10 | ///
11 | /// Reports a . Handling of the
12 | /// may defer between implementations.
13 | ///
14 | ///
15 | /// The of the error being reported.
16 | ///
17 | /// The id of the error.
18 | ///
19 | /// The message to display in the
20 | /// when parsing is completed.
21 | ///
22 | void ReportParseError(
23 | IScriptExtent extent,
24 | string id,
25 | string message);
26 |
27 | ///
28 | /// Throws if the error limit has been hit. Error limit may vary
29 | /// between implementations.
30 | ///
31 | void ThrowIfErrorLimitHit();
32 |
33 | ///
34 | /// Throws if any error has been reported.
35 | ///
36 | void ThrowIfAnyErrors();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: 1.0.0.{build}
2 | image: Visual Studio 2017
3 | environment:
4 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
5 | DOTNET_CLI_TELEMETRY_OPTOUT: true
6 | install:
7 | - ps: >-
8 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
9 |
10 | Install-Module Pester -RequiredVersion 4.3.1 -Scope CurrentUser -Force -SkipPublisherCheck
11 |
12 | Install-Module InvokeBuild -RequiredVersion 5.4.1 -Scope CurrentUser -Force
13 |
14 | Install-Module platyPS -RequiredVersion 0.9.0 -Scope CurrentUser -Force
15 |
16 | choco install codecov --no-progress
17 | build_script:
18 | - ps: >-
19 | . "$PWD\build.ps1" -Force
20 |
21 | $resultsFile = "$PWD\testresults\pester.xml"
22 |
23 | $passed = (Test-Path $resultsFile) -and 0 -eq ([int]([xml](Get-Content $resultsFile -Raw)).'test-results'.failures)
24 |
25 |
26 | if ($passed) {
27 | [System.Net.WebClient]::new().UploadFile("https://ci.appveyor.com/api/testresults/nunit/${env:APPVEYOR_JOB_ID}", $resultsFile)
28 | codecov -f "$PWD\testresults\opencover.xml"
29 | } else {
30 | $Error | Format-List * -Force
31 | exit 1;
32 | }
33 | on_finish:
34 | - ps: >-
35 | Add-Type -AssemblyName System.IO.Compression.FileSystem
36 |
37 | $zipPath = "$pwd\PSLambda.zip"
38 |
39 | [System.IO.Compression.ZipFile]::CreateFromDirectory("$pwd\Release", $zipPath)
40 |
41 | Push-AppveyorArtifact $zipPath
42 |
--------------------------------------------------------------------------------
/ScriptAnalyzerSettings.psd1:
--------------------------------------------------------------------------------
1 | # The PowerShell Script Analyzer will generate a warning
2 | # diagnostic record for this file due to a bug -
3 | # https://github.com/PowerShell/PSScriptAnalyzer/issues/472
4 | @{
5 | # Only diagnostic records of the specified severity will be generated.
6 | # Uncomment the following line if you only want Errors and Warnings but
7 | # not Information diagnostic records.
8 |
9 | # Severity = @('Error','Warning')
10 |
11 | # Analyze **only** the following rules. Use IncludeRules when you want
12 | # to invoke only a small subset of the defualt rules.
13 |
14 | # IncludeRules = @('PSAvoidDefaultValueSwitchParameter',
15 | # 'PSMissingModuleManifestField',
16 | # 'PSReservedCmdletChar',
17 | # 'PSReservedParams',
18 | # 'PSShouldProcess',
19 | # 'PSUseApprovedVerbs',
20 | # 'PSUseDeclaredVarsMoreThanAssigments')
21 |
22 | # Do not analyze the following rules. Use ExcludeRules when you have
23 | # commented out the IncludeRules settings above and want to include all
24 | # the default rules except for those you exclude below.
25 | # Note: if a rule is in both IncludeRules and ExcludeRules, the rule
26 | # will be excluded.
27 |
28 | # ExcludeRules = @('PSAvoidUsingWriteHost')
29 | ExcludeRules = 'PSUseShouldProcessForStateChangingFunctions'
30 | }
31 |
--------------------------------------------------------------------------------
/src/PSLambda/LoopScopeStack.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 |
4 | namespace PSLambda
5 | {
6 | ///
7 | /// Represents a set of scopes in which the break or continue keywords
8 | /// may be used.
9 | ///
10 | internal class LoopScopeStack
11 | {
12 | private LoopScope _current;
13 |
14 | ///
15 | /// Gets the current label for the break keyword.
16 | ///
17 | internal LabelTarget Break => _current?.Break;
18 |
19 | ///
20 | /// Gets the current label for the continue keyword.
21 | ///
22 | internal LabelTarget Continue => _current?.Continue;
23 |
24 | ///
25 | /// Creates a new scope in which the break or continue keywords
26 | /// may be used.
27 | ///
28 | ///
29 | /// A handle that will return to the previous scope when disposed.
30 | ///
31 | internal IDisposable NewScope()
32 | {
33 | _current = new LoopScope()
34 | {
35 | Parent = _current,
36 | Break = Expression.Label(),
37 | Continue = Expression.Label(),
38 | };
39 |
40 | return new ScopeHandle(() => _current = _current?.Parent);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/PSLambda/PSVariableWrapper.cs:
--------------------------------------------------------------------------------
1 | using System.Management.Automation;
2 |
3 | namespace PSLambda
4 | {
5 | ///
6 | /// Provides a strongly typed wrapper for objects.
7 | ///
8 | /// The type of object contained by the .
9 | internal class PSVariableWrapper
10 | {
11 | private readonly object _syncObject = new object();
12 |
13 | private readonly PSVariable _wrappedVariable;
14 |
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | /// The to wrap.
19 | public PSVariableWrapper(PSVariable variable)
20 | {
21 | _wrappedVariable = variable;
22 | }
23 |
24 | ///
25 | /// Gets or sets the value of the object.
26 | ///
27 | public TValue Value
28 | {
29 | get
30 | {
31 | lock (_syncObject)
32 | {
33 | return LanguagePrimitives.ConvertTo(_wrappedVariable.Value);
34 | }
35 | }
36 |
37 | set
38 | {
39 | lock (_syncObject)
40 | {
41 | _wrappedVariable.Value = value;
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/PSLambda/VariableUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Management.Automation.Language;
3 |
4 | namespace PSLambda
5 | {
6 | ///
7 | /// Provides utility methods for variable expression operations.
8 | ///
9 | internal static class VariableUtils
10 | {
11 | ///
12 | /// Determines if the specified variable is referencing the automatic
13 | /// variable $_ or $PSItem.
14 | ///
15 | /// The variable expression to test.
16 | ///
17 | /// if the specified variable references
18 | /// dollar under, otherwise .
19 | ///
20 | public static bool IsDollarUnder(VariableExpressionAst variableExpressionAst)
21 | => IsDollarUnder(variableExpressionAst.VariablePath.UserPath);
22 |
23 | ///
24 | /// Determines if the specified variable is referencing the automatic
25 | /// variable $_ or $PSItem.
26 | ///
27 | /// The variable name to test.
28 | ///
29 | /// if the specified variable references
30 | /// dollar under, otherwise .
31 | ///
32 | public static bool IsDollarUnder(string variableName)
33 | {
34 | return variableName.Equals("_", StringComparison.Ordinal)
35 | || variableName.Equals("PSItem", StringComparison.Ordinal);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/PSLambda/Empty.cs:
--------------------------------------------------------------------------------
1 | using System.Management.Automation.Language;
2 |
3 | namespace PSLambda
4 | {
5 | ///
6 | /// Provides utility methods for empty elements.
7 | ///
8 | internal static class Empty
9 | {
10 | ///
11 | /// Gets an empty .
12 | ///
13 | internal static IScriptExtent Extent => new EmptyScriptExtent();
14 |
15 | ///
16 | /// Gets an empty .
17 | ///
18 | internal static IScriptPosition Position => new EmptyScriptPosition();
19 |
20 | private class EmptyScriptExtent : IScriptExtent
21 | {
22 | public int EndColumnNumber => 0;
23 |
24 | public int EndLineNumber => 0;
25 |
26 | public int EndOffset => 0;
27 |
28 | public IScriptPosition EndScriptPosition => Position;
29 |
30 | public string File => string.Empty;
31 |
32 | public int StartColumnNumber => 0;
33 |
34 | public int StartLineNumber => 0;
35 |
36 | public int StartOffset => 0;
37 |
38 | public IScriptPosition StartScriptPosition => Position;
39 |
40 | public string Text => string.Empty;
41 | }
42 |
43 | private class EmptyScriptPosition : IScriptPosition
44 | {
45 | public int ColumnNumber => 0;
46 |
47 | public string File => string.Empty;
48 |
49 | public string Line => string.Empty;
50 |
51 | public int LineNumber => 0;
52 |
53 | public int Offset => 0;
54 |
55 | public string GetFullScript() => string.Empty;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/PSLambda.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio 15
3 | VisualStudioVersion = 15.0.26124.0
4 | MinimumVisualStudioVersion = 15.0.26124.0
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSLambda", "src\PSLambda\PSLambda.csproj", "{6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Debug|x64 = Debug|x64
11 | Debug|x86 = Debug|x86
12 | Release|Any CPU = Release|Any CPU
13 | Release|x64 = Release|x64
14 | Release|x86 = Release|x86
15 | EndGlobalSection
16 | GlobalSection(SolutionProperties) = preSolution
17 | HideSolutionNode = FALSE
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x64.ActiveCfg = Debug|x64
23 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x64.Build.0 = Debug|x64
24 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x86.ActiveCfg = Debug|x86
25 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Debug|x86.Build.0 = Debug|x86
26 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x64.ActiveCfg = Release|x64
29 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x64.Build.0 = Release|x64
30 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x86.ActiveCfg = Release|x86
31 | {6b92400e-adfe-4fa8-8c10-7c0d47d6a5f4}.Release|x86.Build.0 = Release|x86
32 | EndGlobalSection
33 | EndGlobal
34 |
35 |
--------------------------------------------------------------------------------
/src/PSLambda/LabelScope.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 |
4 | namespace PSLambda
5 | {
6 | ///
7 | /// Represents the a scope in which the return keyword will work.
8 | ///
9 | internal class LabelScope
10 | {
11 | ///
12 | /// The parent scope.
13 | ///
14 | internal readonly LabelScope _parent;
15 |
16 | private LabelTarget _label;
17 |
18 | ///
19 | /// Initializes a new instance of the class.
20 | ///
21 | internal LabelScope()
22 | {
23 | }
24 |
25 | ///
26 | /// Initializes a new instance of the class.
27 | ///
28 | /// The parent .
29 | internal LabelScope(LabelScope parent)
30 | {
31 | _parent = parent;
32 | }
33 |
34 | ///
35 | /// Gets or sets a value indicating whether an explicit return statement has been used.
36 | ///
37 | public bool IsReturnRequested { get; set; }
38 |
39 | ///
40 | /// Gets or sets the implied or explicit return type.
41 | ///
42 | public Type ReturnType { get; set; }
43 |
44 | ///
45 | /// Gets the current return label.
46 | ///
47 | public LabelTarget Label
48 | {
49 | get
50 | {
51 | if (_label != null)
52 | {
53 | return _label;
54 | }
55 |
56 | _label = Expression.Label(ReturnType);
57 | return _label;
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/PSLambda/PSLambdaAssemblyInitializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Management.Automation;
4 |
5 | namespace PSLambda
6 | {
7 | ///
8 | /// Provides initialization services when the module is imported.
9 | ///
10 | public class PSLambdaAssemblyInitializer : IModuleAssemblyInitializer
11 | {
12 | private const string TypeAcceleratorTypeName = "System.Management.Automation.TypeAccelerators";
13 |
14 | private const string GetPropertyName = "Get";
15 |
16 | ///
17 | /// Attempts to create the type accelerator for .
18 | ///
19 | public void OnImport()
20 | {
21 | var accelType = typeof(PSObject).Assembly.GetType(TypeAcceleratorTypeName);
22 | if (accelType == null)
23 | {
24 | return;
25 | }
26 |
27 | var getProperty = accelType.GetProperty(GetPropertyName);
28 | if (getProperty == null)
29 | {
30 | return;
31 | }
32 |
33 | if (!(getProperty.GetValue(null) is Dictionary existing))
34 | {
35 | return;
36 | }
37 |
38 | if (existing.ContainsKey(Strings.PSDelegateTypeAcceleratorName))
39 | {
40 | return;
41 | }
42 |
43 | var addMethod = accelType.GetMethod(
44 | Strings.AddMethodName,
45 | new[] { typeof(string), typeof(Type) });
46 | if (addMethod == null)
47 | {
48 | return;
49 | }
50 |
51 | try
52 | {
53 | addMethod.Invoke(
54 | null,
55 | new object[] { Strings.PSDelegateTypeAcceleratorName, typeof(PSDelegate) });
56 | }
57 | catch (Exception)
58 | {
59 | return;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/WithCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using System.Management.Automation.Language;
4 |
5 | namespace PSLambda.Commands
6 | {
7 | ///
8 | /// Provides handling for the "with" custom command.
9 | ///
10 | internal class WithCommand : ObjectAndBodyCommandHandler
11 | {
12 | ///
13 | /// Gets the name of the command.
14 | ///
15 | public override string CommandName { get; } = "with";
16 |
17 | ///
18 | /// Creates a Linq expression for a representing
19 | /// the "with" command.
20 | ///
21 | /// The AST to convert.
22 | /// The AST containing the target of the keyword.
23 | /// The AST containing the body of the keyword.
24 | /// The requesting the expression.
25 | /// An expression representing the command.
26 | protected override Expression ProcessObjectAndBody(
27 | CommandAst commandAst,
28 | CommandElementAst targetAst,
29 | ScriptBlockExpressionAst bodyAst,
30 | CompileVisitor visitor)
31 | {
32 | var disposeVar = Expression.Variable(typeof(IDisposable));
33 | return visitor.NewBlock(() =>
34 | Expression.Block(
35 | typeof(void),
36 | new[] { disposeVar },
37 | Expression.Assign(
38 | disposeVar,
39 | Expression.Convert(
40 | targetAst.Compile(visitor),
41 | typeof(IDisposable))),
42 | Expression.TryFinally(
43 | bodyAst.ScriptBlock.EndBlock.Compile(visitor),
44 | Expression.Call(disposeVar, ReflectionCache.IDisposable_Dispose))));
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/PSDelegate.Tests.ps1:
--------------------------------------------------------------------------------
1 | using namespace System.Collections.Generic
2 | using namespace System.Threading.Tasks
3 |
4 | $moduleName = 'PSLambda'
5 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
6 |
7 | Import-Module $manifestPath -Force
8 |
9 | Describe 'Delegate conversion' {
10 | It 'can handle Task.Run' {
11 | & {
12 | $myVar = 'test'
13 | [Task]::Run([action][psdelegate]{ $myVar = 'newValue' }).GetAwaiter().GetResult()
14 | $myVar | Should -Be 'newValue'
15 | }
16 | }
17 |
18 | It 'can infer parameters from conversion' {
19 | $originalObject = [object]::new()
20 | $task = [Task[psobject]]::Factory.StartNew(
21 | [psdelegate]{ ($state) => { [PSObject]::AsPSObject($state) }},
22 | $originalObject)
23 |
24 | $newObject = $task.GetAwaiter().GetResult()
25 | $originalObject.GetHashCode() | Should -Be $newObject.psobject.BaseObject.GetHashCode()
26 | }
27 |
28 | It 'retains locals from origin scope' {
29 | $delegate = & {
30 | $myVar = 'goodValue'
31 | [psdelegate]{ $myVar }
32 | }
33 |
34 | $myVar = 'badValue'
35 | $delegate.Invoke() | Should -Be 'goodValue'
36 | }
37 |
38 | It 'changes locals from origin scope' {
39 | & {
40 | $myVar = 'oldValue'
41 | $delegate = [psdelegate]{ $myVar = 'newValue' }
42 | & {
43 | $myVar = 'newScopeValue'
44 | $delegate.Invoke()
45 | $myVar | Should -Be 'newScopeValue'
46 | }
47 |
48 | $myVar | Should -Be 'newValue'
49 | }
50 | }
51 |
52 | It 'can be converted inside existing delegate with convert expression' {
53 | $task = [Task[int]]::Factory.StartNew([psdelegate]{
54 | $total = 0
55 | $list = [List[int]]::new(0..5)
56 | $list.ForEach([action[int]]{ ($i) => { $total = $total + $i }})
57 | return $total
58 | })
59 |
60 | $task.GetAwaiter().GetResult() | Should -Be 15
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tools/GetOpenCover.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
2 | param(
3 | [string] $Version = '4.6.519',
4 | [switch] $Force
5 | )
6 | end {
7 | function GetVersionNumber {
8 | param([System.Management.Automation.CommandInfo] $Command)
9 | end {
10 | return (& $Command -version) `
11 | -replace 'OpenCover version ' `
12 | -replace '\.0$'
13 | }
14 | }
15 |
16 | $TARGET_FOLDER = "$PSScriptRoot\opencover"
17 | $TARGET_ARCHIVE = "$PSScriptRoot\opencover.zip"
18 | $TARGET_NAME = 'OpenCover.Console.exe'
19 |
20 | $ErrorActionPreference = 'Stop'
21 |
22 | if ($openCover = Get-Command $TARGET_FOLDER\$TARGET_NAME -ea 0) {
23 | if (($found = GetVersionNumber $openCover) -eq $Version) {
24 | return $openCover
25 | }
26 |
27 | Write-Host -ForegroundColor Yellow Found OpenCover $found but require $Version, replacing...
28 |
29 | if ($Force.IsPresent -or $PSCmdlet.ShouldProcess($TARGET_FOLDER, 'Remove-Item')) {
30 | Remove-Item $TARGET_FOLDER -Recurse
31 | } else {
32 | throw 'Existing version of OpenCover must be removed before it can be redownloaded.'
33 | }
34 | }
35 | Write-Host -ForegroundColor Green Downloading OpenCover version $Version
36 |
37 | $url = "https://github.com/OpenCover/opencover/releases/download/$Version/opencover.$Version.zip"
38 | if ($Force.IsPresent -or $PSCmdlet.ShouldProcess($url, 'Invoke-WebRequest')) {
39 | $oldSecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol
40 | try {
41 | [System.Net.ServicePointManager]::SecurityProtocol = 'Tls, Tls11, Tls12'
42 | Invoke-WebRequest $url -OutFile $TARGET_ARCHIVE
43 | } finally {
44 | [System.Net.ServicePointManager]::SecurityProtocol = $oldSecurityProtocol
45 | }
46 |
47 | Expand-Archive $TARGET_ARCHIVE -DestinationPath $TARGET_FOLDER -Force
48 | Remove-Item $TARGET_ARCHIVE
49 | } else {
50 | throw 'OpenCover is required to generate code coverage.'
51 | }
52 |
53 |
54 | return Get-Command $TARGET_FOLDER\$TARGET_NAME
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/PSLambda/PSLambda.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/test/Commands.Tests.ps1:
--------------------------------------------------------------------------------
1 | using namespace System.Collections.ObjectModel
2 | using namespace System.Diagnostics
3 | using namespace System.Management.Automation
4 | using namespace System.Threading.Tasks
5 |
6 | $moduleName = 'PSLambda'
7 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
8 |
9 | Import-Module $manifestPath -Force
10 |
11 | Describe 'Custom command handlers' {
12 | It 'with' {
13 | $delegate = [psdelegate]{
14 | $ps = [powershell]::Create([RunspaceMode]::NewRunspace)
15 | with ($ps) {
16 | $ps.AddScript('$ExecutionContext').Invoke()
17 | }
18 |
19 | if ($ps.Runspace.RunspaceStateInfo.State -ne [Runspaces.RunspaceState]::Closed) {
20 | throw [RuntimeException]::new('Runspace was not closed after the with statement ended.')
21 | }
22 | }
23 |
24 | $delegate.Invoke()
25 | }
26 |
27 | It 'generic' {
28 | ([psdelegate]{ generic([array]::Empty(), [int]) }).Invoke().GetType() | Should -Be ([int[]])
29 | }
30 |
31 | It 'default' {
32 | ([psdelegate]{ default([ConsoleColor]) }).Invoke() | Should -Be ([System.ConsoleColor]::Black)
33 | }
34 |
35 | It 'lock' {
36 | # Need a way better way to test this
37 | $delegate = New-PSDelegate {
38 | $syncObject = [object]::new()
39 | $collection = [Collection[TimeSpan]]::new()
40 | $stopWatch = [Stopwatch]::StartNew()
41 |
42 | $tasks = [Collection[Task]]::new()
43 | for ($i = 0; $i -lt 3; $i++) {
44 | $tasks.Add(
45 | [Task]::Run({
46 | lock ($syncObject) {
47 | [System.Threading.Thread]::Sleep(100)
48 | $collection.Add($stopWatch.Elapsed)
49 | $stopWatch.Restart()
50 | }
51 | }))
52 | }
53 |
54 | [Task]::WaitAll($tasks.ToArray())
55 | return $collection
56 | }
57 |
58 | $times = $delegate.Invoke()
59 | foreach ($time in $times) {
60 | $time.TotalMilliseconds | Should -BeGreaterThan 100
61 | $time.TotalMilliseconds | Should -BeLessThan 150
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/LockCommand.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using System.Management.Automation.Language;
3 |
4 | namespace PSLambda.Commands
5 | {
6 | ///
7 | /// Provides handling for the "lock" custom command.
8 | ///
9 | internal class LockCommand : ObjectAndBodyCommandHandler
10 | {
11 | ///
12 | /// Gets the name of the command.
13 | ///
14 | public override string CommandName { get; } = "lock";
15 |
16 | ///
17 | /// Creates a Linq expression for a representing
18 | /// the "lock" command.
19 | ///
20 | /// The AST to convert.
21 | /// The AST containing the target of the keyword.
22 | /// The AST containing the body of the keyword.
23 | /// The requesting the expression.
24 | /// An expression representing the command.
25 | protected override Expression ProcessObjectAndBody(
26 | CommandAst commandAst,
27 | CommandElementAst targetAst,
28 | ScriptBlockExpressionAst bodyAst,
29 | CompileVisitor visitor)
30 | {
31 | var lockVar = Expression.Variable(typeof(object));
32 | var lockTakenVar = Expression.Variable(typeof(bool));
33 | return visitor.NewBlock(() =>
34 | Expression.Block(
35 | typeof(void),
36 | new[] { lockVar, lockTakenVar },
37 | Expression.Call(
38 | ReflectionCache.Monitor_Enter,
39 | Expression.Assign(
40 | lockVar,
41 | Expression.Convert(
42 | targetAst.Compile(visitor),
43 | typeof(object))),
44 | lockTakenVar),
45 | Expression.TryFinally(
46 | bodyAst.ScriptBlock.EndBlock.Compile(visitor),
47 | Expression.IfThen(
48 | lockTakenVar,
49 | Expression.Call(
50 | ReflectionCache.Monitor_Exit,
51 | lockVar)))));
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/CommandService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq.Expressions;
4 | using System.Management.Automation.Language;
5 |
6 | namespace PSLambda.Commands
7 | {
8 | ///
9 | /// Provides management of custom command handlers.
10 | ///
11 | internal class CommandService
12 | {
13 | private readonly Dictionary _commandRegistry = new Dictionary(StringComparer.OrdinalIgnoreCase);
14 |
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | public CommandService()
19 | {
20 | RegisterCommand(new DefaultCommand());
21 | RegisterCommand(new WithCommand());
22 | RegisterCommand(new GenericCommand());
23 | RegisterCommand(new LockCommand());
24 | }
25 |
26 | ///
27 | /// Registers a command handler.
28 | ///
29 | /// The handler to register.
30 | public void RegisterCommand(ICommandHandler handler)
31 | {
32 | if (_commandRegistry.ContainsKey(handler.CommandName))
33 | {
34 | return;
35 | }
36 |
37 | _commandRegistry.Add(handler.CommandName, handler);
38 | }
39 |
40 | ///
41 | /// Attempt to process a as a custom command.
42 | ///
43 | /// The to process.
44 | ///
45 | /// The requesting the expression.
46 | ///
47 | ///
48 | /// The result if a command handler was found.
49 | ///
50 | /// true if a commmand handler was matched, otherwise false.
51 | public bool TryProcessAst(CommandAst commandAst, CompileVisitor visitor, out Expression expression)
52 | {
53 | if (_commandRegistry.TryGetValue(commandAst.GetCommandName(), out ICommandHandler handler))
54 | {
55 | expression = handler.ProcessAst(commandAst, visitor);
56 | return true;
57 | }
58 |
59 | expression = null;
60 | return false;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // Available variables which can be used inside of strings.
2 | // ${workspaceRoot}: the root folder of the team
3 | // ${file}: the current opened file
4 | // ${relativeFile}: the current opened file relative to workspaceRoot
5 | // ${fileBasename}: the current opened file's basename
6 | // ${fileDirname}: the current opened file's dirname
7 | // ${fileExtname}: the current opened file's extension
8 | // ${cwd}: the current working directory of the spawned process
9 | {
10 | // See https://go.microsoft.com/fwlink/?LinkId=733558
11 | // for the documentation about the tasks.json format
12 | "version": "2.0.0",
13 | "windows": {
14 | "options": {
15 | "shell": {
16 | "executable": "powershell.exe",
17 | "args": [ "-ExecutionPolicy Bypass", "-NoProfile", "-Command" ],
18 | },
19 | },
20 | },
21 | "linux": {
22 | "options": {
23 | "shell": {
24 | "executable": "/usr/bin/pwsh",
25 | "args": [ "-NoProfile", "-Command" ],
26 | },
27 | },
28 | },
29 | "osx": {
30 | "options": {
31 | "shell": {
32 | "executable": "/usr/local/bin/pwsh",
33 | "args": [ "-NoProfile", "-Command" ],
34 | },
35 | },
36 | },
37 | "type": "shell",
38 | "tasks": [
39 | {
40 | "label": "Clean",
41 | "command": "Invoke-Build",
42 | "args": [ "-Task", "Clean" ],
43 | },
44 | {
45 | "label": "Quick Build",
46 | "command": "dotnet",
47 | "args": [ "build", "/property:GenerateFullPaths=true" ],
48 | "group": { "kind": "build", "isDefault": true, },
49 | "problemMatcher": "$msCompile",
50 | },
51 | {
52 | "label": "Build",
53 | "command": "& ./build.ps1 -Configuration Release -Build",
54 | "group": "build",
55 | },
56 | {
57 | "label": "Test",
58 | "command": "& ./build.ps1 -Configuration Release -Test",
59 | "group": { "kind": "test", "isDefault": true, },
60 | },
61 | {
62 | "label": "Test With Coverage",
63 | "command": "& ./build.ps1 -Configuration Debug -Test -GenerateCodeCoverage",
64 | "group": "test",
65 | },
66 | {
67 | "label": "Install",
68 | "command": "& ./build.ps1 -Configuration Release -Task Install",
69 | }
70 | ]
71 | }
72 |
--------------------------------------------------------------------------------
/src/PSLambda/ExpressionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq.Expressions;
4 | using System.Management.Automation.Language;
5 |
6 | namespace PSLambda
7 | {
8 | ///
9 | /// Provides utility methods for creating objects.
10 | ///
11 | internal static class ExpressionExtensions
12 | {
13 | private static readonly Expression[] s_emptyExpressions = new Expression[0];
14 |
15 | ///
16 | /// Compile all asts in a given .
17 | ///
18 | /// The objects to compile.
19 | /// The requesting the compile.
20 | /// The type of to expect in the list.
21 | /// The compiled objects.
22 | public static Expression[] CompileAll(this IList asts, CompileVisitor visitor)
23 | where TAst : Ast
24 | {
25 | if (asts == null || asts.Count == 0)
26 | {
27 | return s_emptyExpressions;
28 | }
29 |
30 | var expressions = new Expression[asts.Count];
31 | for (var i = 0; i < asts.Count; i++)
32 | {
33 | expressions[i] = (Expression)asts[i].Visit(visitor);
34 | }
35 |
36 | return expressions;
37 | }
38 |
39 | ///
40 | /// Compile a into a .
41 | ///
42 | /// The to compile.
43 | /// The requesting the compile.
44 | /// The compiled object.
45 | public static Expression Compile(this Ast ast, CompileVisitor visitor)
46 | {
47 | try
48 | {
49 | return (Expression)ast.Visit(visitor);
50 | }
51 | catch (ArgumentException e)
52 | {
53 | visitor.Errors.ReportParseError(ast.Extent, e);
54 | return Expression.Empty();
55 | }
56 | catch (InvalidOperationException e)
57 | {
58 | visitor.Errors.ReportParseError(ast.Extent, e);
59 | return Expression.Empty();
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/DefaultCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using System.Management.Automation;
4 | using System.Management.Automation.Language;
5 |
6 | namespace PSLambda.Commands
7 | {
8 | ///
9 | /// Provides handling for the "default" custom command.
10 | ///
11 | internal class DefaultCommand : ICommandHandler
12 | {
13 | ///
14 | /// Gets the name of the command.
15 | ///
16 | public string CommandName { get; } = "default";
17 |
18 | ///
19 | /// Creates a Linq expression for a representing
20 | /// the "default" command.
21 | ///
22 | /// The AST to convert.
23 | /// The requesting the expression.
24 | /// An expression representing the command.
25 | public Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor)
26 | {
27 | if (commandAst.CommandElements == null || commandAst.CommandElements.Count != 2)
28 | {
29 | visitor.TryResolveType(null, out _);
30 | return Expression.Empty();
31 | }
32 |
33 | // Is using syntax "default([TypeName])"
34 | if (commandAst.CommandElements[1] is ParenExpressionAst paren &&
35 | paren.Pipeline is PipelineAst pipeline &&
36 | pipeline.PipelineElements.Count == 1 &&
37 | pipeline.PipelineElements[0] is CommandExpressionAst commandExpression &&
38 | commandExpression.Expression is TypeExpressionAst &&
39 | visitor.TryResolveType(commandExpression.Expression, out Type resolvedType))
40 | {
41 | return Expression.Default(resolvedType);
42 | }
43 |
44 | // Is using syntax "default TypeName"
45 | if (commandAst.CommandElements[1] is StringConstantExpressionAst stringConstant &&
46 | LanguagePrimitives.TryConvertTo(stringConstant.Value, out resolvedType))
47 | {
48 | return Expression.Default(resolvedType);
49 | }
50 |
51 | // Unknown syntax, but this method will produce parse errors for us.
52 | if (visitor.TryResolveType(commandAst.CommandElements[1], out resolvedType))
53 | {
54 | return Expression.Default(resolvedType);
55 | }
56 |
57 | return Expression.Empty();
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/PSLambda/NewPSDelegateCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Management.Automation;
4 | using System.Management.Automation.Language;
5 | using System.Management.Automation.Runspaces;
6 |
7 | namespace PSLambda
8 | {
9 | ///
10 | /// Represents the New-PSDelegate command.
11 | ///
12 | [Cmdlet(VerbsCommon.New, "PSDelegate")]
13 | [OutputType(typeof(Delegate))]
14 | public class NewPSDelegateCommand : PSCmdlet
15 | {
16 | ///
17 | /// Gets or sets the value for the parameter "DelegateType".
18 | ///
19 | [Parameter]
20 | [ValidateNotNull]
21 | public Type DelegateType { get; set; }
22 |
23 | ///
24 | /// Gets or sets the value for the parameter "Expression".
25 | ///
26 | [Parameter(Position = 0, Mandatory = true)]
27 | [ValidateNotNull]
28 | public ScriptBlock Expression { get; set; }
29 |
30 | ///
31 | /// The EndProcessing method.
32 | ///
33 | protected override void EndProcessing()
34 | {
35 | var variables = SessionState.InvokeCommand.InvokeScript(
36 | "Get-Variable -Scope 0",
37 | false,
38 | PipelineResultTypes.Output,
39 | null,
40 | null)
41 | .Select(pso => pso.BaseObject)
42 | .Cast()
43 | .Where(v => !SpecialVariables.IgnoreLocal.Contains(v.Name));
44 |
45 | try
46 | {
47 | if (DelegateType == null)
48 | {
49 | WriteObject(
50 | CompileVisitor.CompileAst(
51 | (EngineIntrinsics)SessionState.PSVariable.GetValue(Strings.ExecutionContextVariableName),
52 | (ScriptBlockAst)Expression.Ast,
53 | variables.ToArray()),
54 | enumerateCollection: false);
55 | return;
56 | }
57 |
58 | WriteObject(
59 | CompileVisitor.CompileAst(
60 | (EngineIntrinsics)SessionState.PSVariable.GetValue(Strings.ExecutionContextVariableName),
61 | (ScriptBlockAst)Expression.Ast,
62 | variables.ToArray(),
63 | DelegateType),
64 | enumerateCollection: false);
65 | }
66 | catch (ParseException e)
67 | {
68 | ThrowTerminatingError(new ErrorRecord(e.ErrorRecord, e));
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/test/IfStatements.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Import-Module $manifestPath -Force
5 |
6 | Describe 'If statements' {
7 | It 'if statement only' {
8 | $delegate = New-PSDelegate {
9 | ([int] $a) => {
10 | if ($a -eq 1) {
11 | return $true
12 | }
13 |
14 | return $false
15 | }
16 | }
17 |
18 | $delegate.Invoke(1) | Should -Be $true
19 | $delegate.Invoke(0) | Should -Be $false
20 | }
21 |
22 | It 'if else' {
23 | $delegate = New-PSDelegate {
24 | ([int] $a) => {
25 | if ($a -eq 1) {
26 | return 'if'
27 | } else {
28 | return 'else'
29 | }
30 | }
31 | }
32 |
33 | $delegate.Invoke(1) | Should -Be 'if'
34 | $delegate.Invoke(0) | Should -Be 'else'
35 | }
36 |
37 | It 'if else if' {
38 | $delegate = New-PSDelegate {
39 | ([int] $a) => {
40 | if ($a -eq 1) {
41 | return 'if'
42 | } elseif ($a -eq 2) {
43 | return 'elseif'
44 | }
45 |
46 | return 'none'
47 | }
48 | }
49 |
50 | $delegate.Invoke(2) | Should -Be 'elseif'
51 | $delegate.Invoke(1) | Should -Be 'if'
52 | $delegate.Invoke(0) | Should -Be 'none'
53 | }
54 |
55 | It 'if else-if else' {
56 | $delegate = New-PSDelegate {
57 | ([int] $a) => {
58 | if ($a -eq 1) {
59 | return 'if'
60 | } elseif ($a -eq 2) {
61 | return 'elseif'
62 | } else {
63 | return 'else'
64 | }
65 | }
66 | }
67 |
68 | $delegate.Invoke(2) | Should -Be 'elseif'
69 | $delegate.Invoke(1) | Should -Be 'if'
70 | $delegate.Invoke(0) | Should -Be 'else'
71 | }
72 |
73 | It 'if with multiple else-ifs' {
74 | $delegate = New-PSDelegate {
75 | ([int] $a) => {
76 | if ($a -eq 1) {
77 | return 'if'
78 | } elseif ($a -eq 2) {
79 | return 'elseif'
80 | } elseif ($a -eq 3) {
81 | return 'elseif1'
82 | } elseif ($a -eq 4) {
83 | return 'elseif2'
84 | } else {
85 | return 'else'
86 | }
87 | }
88 | }
89 |
90 | $delegate.Invoke(4) | Should -Be 'elseif2'
91 | $delegate.Invoke(3) | Should -Be 'elseif1'
92 | $delegate.Invoke(2) | Should -Be 'elseif'
93 | $delegate.Invoke(1) | Should -Be 'if'
94 | $delegate.Invoke(0) | Should -Be 'else'
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/ObjectAndBodyCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Linq.Expressions;
3 | using System.Management.Automation.Language;
4 |
5 | namespace PSLambda.Commands
6 | {
7 | ///
8 | /// Provides a base for commands that follow the format of
9 | /// `commandName ($objectTarget) { body }`.
10 | ///
11 | internal abstract class ObjectAndBodyCommandHandler : ICommandHandler
12 | {
13 | ///
14 | /// Gets the name of the command.
15 | ///
16 | public abstract string CommandName { get; }
17 |
18 | ///
19 | /// Creates a Linq expression for a representing
20 | /// a custom command.
21 | ///
22 | /// The AST to convert.
23 | /// The requesting the expression.
24 | /// An expression representing the command.
25 | public Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor)
26 | {
27 | if (commandAst.CommandElements.Count != 3)
28 | {
29 | visitor.Errors.ReportParseError(
30 | commandAst.Extent,
31 | nameof(ErrorStrings.MissingKeywordElements),
32 | string.Format(
33 | CultureInfo.CurrentCulture,
34 | ErrorStrings.MissingKeywordElements,
35 | CommandName));
36 | return Expression.Empty();
37 | }
38 |
39 | if (!(commandAst.CommandElements[2] is ScriptBlockExpressionAst bodyAst))
40 | {
41 | visitor.Errors.ReportParseError(
42 | commandAst.Extent,
43 | nameof(ErrorStrings.MissingKeywordBody),
44 | string.Format(
45 | CultureInfo.CurrentCulture,
46 | ErrorStrings.MissingKeywordBody,
47 | CommandName));
48 | return Expression.Empty();
49 | }
50 |
51 | return ProcessObjectAndBody(
52 | commandAst,
53 | commandAst.CommandElements[1],
54 | bodyAst,
55 | visitor);
56 | }
57 |
58 | ///
59 | /// Creates a Linq expression for a representing
60 | /// a custom command.
61 | ///
62 | /// The AST to convert.
63 | /// The AST containing the target of the keyword.
64 | /// The AST containing the body of the keyword.
65 | /// The requesting the expression.
66 | /// An expression representing the command.
67 | protected abstract Expression ProcessObjectAndBody(
68 | CommandAst commandAst,
69 | CommandElementAst targetAst,
70 | ScriptBlockExpressionAst bodyAst,
71 | CompileVisitor visitor);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/PSLambda/Commands/GenericCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using System.Management.Automation.Language;
4 |
5 | namespace PSLambda.Commands
6 | {
7 | ///
8 | /// Provides handling for the "generic" custom command.
9 | ///
10 | internal class GenericCommand : ICommandHandler
11 | {
12 | ///
13 | /// Gets the name of the command.
14 | ///
15 | public string CommandName { get; } = "generic";
16 |
17 | ///
18 | /// Creates a Linq expression for a representing
19 | /// the "generic" command.
20 | ///
21 | /// The AST to convert.
22 | /// The requesting the expression.
23 | /// An expression representing the command.
24 | public Expression ProcessAst(CommandAst commandAst, CompileVisitor visitor)
25 | {
26 | if (commandAst.CommandElements.Count != 2)
27 | {
28 | return ReportInvalidSyntax(commandAst.Extent, visitor);
29 | }
30 |
31 | if (!(commandAst.CommandElements[1] is ParenExpressionAst paren))
32 | {
33 | return ReportInvalidSyntax(commandAst.Extent, visitor);
34 | }
35 |
36 | if (!(paren.Pipeline is PipelineAst pipeline) || pipeline.PipelineElements.Count != 1)
37 | {
38 | return ReportInvalidSyntax(commandAst.Extent, visitor);
39 | }
40 |
41 | if (!(pipeline.PipelineElements[0] is CommandExpressionAst commandExpression))
42 | {
43 | return ReportInvalidSyntax(commandAst.Extent, visitor);
44 | }
45 |
46 | var arrayLiteral = commandExpression.Expression as ArrayLiteralAst;
47 | if (arrayLiteral.Elements.Count < 2)
48 | {
49 | return ReportInvalidSyntax(commandAst.Extent, visitor);
50 | }
51 |
52 | if (!(arrayLiteral.Elements[0] is InvokeMemberExpressionAst memberExpression))
53 | {
54 | return ReportInvalidSyntax(commandAst.Extent, visitor);
55 | }
56 |
57 | var genericArguments = new Type[arrayLiteral.Elements.Count - 1];
58 | for (var i = 1; i < arrayLiteral.Elements.Count; i++)
59 | {
60 | if (visitor.TryResolveType(arrayLiteral.Elements[i], out Type resolvedType))
61 | {
62 | genericArguments[i - 1] = resolvedType;
63 | continue;
64 | }
65 |
66 | // If a type didn't resolve then a parse error was generated, so exit.
67 | return Expression.Empty();
68 | }
69 |
70 | return visitor.CompileInvokeMemberExpression(memberExpression, genericArguments);
71 | }
72 |
73 | private Expression ReportInvalidSyntax(IScriptExtent extent, CompileVisitor visitor)
74 | {
75 | visitor.Errors.ReportParseError(
76 | extent,
77 | nameof(ErrorStrings.InvalidGenericSyntax),
78 | ErrorStrings.InvalidGenericSyntax);
79 | return Expression.Empty();
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/docs/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at seeminglyscience@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/src/PSLambda/Strings.cs:
--------------------------------------------------------------------------------
1 | namespace PSLambda
2 | {
3 | ///
4 | /// Provides string constant values for various strings used throughout the project.
5 | ///
6 | internal class Strings
7 | {
8 | ///
9 | /// Constant containing a string similar to "GetEnumerator".
10 | ///
11 | public const string GetEnumeratorMethodName = "GetEnumerator";
12 |
13 | ///
14 | /// Constant containing a string similar to "get_Current".
15 | ///
16 | public const string EnumeratorGetCurrentMethodName = "get_Current";
17 |
18 | ///
19 | /// Constant containing a string similar to "psdelegate".
20 | ///
21 | public const string PSDelegateTypeAcceleratorName = "psdelegate";
22 |
23 | ///
24 | /// Constant containing a string similar to "ExecutionContext".
25 | ///
26 | public const string ExecutionContextVariableName = "ExecutionContext";
27 |
28 | ///
29 | /// Constant containing a string similar to "foreach".
30 | ///
31 | public const string ForEachVariableName = "foreach";
32 |
33 | ///
34 | /// Constant containing a string similar to "null".
35 | ///
36 | public const string NullVariableName = "null";
37 |
38 | ///
39 | /// Constant containing a string similar to "true".
40 | ///
41 | public const string TrueVariableName = "true";
42 |
43 | ///
44 | /// Constant containing a string similar to "false".
45 | ///
46 | public const string FalseVariableName = "false";
47 |
48 | ///
49 | /// Constant containing a string similar to "new".
50 | ///
51 | public const string ConstructorMemberName = "new";
52 |
53 | ///
54 | /// Constant containing a string similar to "OperatorNotSupported".
55 | ///
56 | public const string OperatorNotSupportedId = "OperatorNotSupported";
57 |
58 | ///
59 | /// Constant containing a string similar to ">".
60 | ///
61 | public const string DelegateSyntaxCommandName = ">";
62 |
63 | ///
64 | /// Constant containing a string similar to "Invoke".
65 | ///
66 | public const string DelegateInvokeMethodName = "Invoke";
67 |
68 | ///
69 | /// Constant containing a string similar to "Value".
70 | ///
71 | public const string PSVariableWrapperValuePropertyName = "Value";
72 |
73 | ///
74 | /// Constant containing a string similar to "Item".
75 | ///
76 | public const string DefaultIndexerPropertyName = "Item";
77 |
78 | ///
79 | /// Constant containing a string similar to "ElementAtOrDefault".
80 | ///
81 | public const string ElementAtOrDefaultMethodName = "ElementAtOrDefault";
82 |
83 | ///
84 | /// Constant containing a string similar to "Add".
85 | ///
86 | public const string AddMethodName = "Add";
87 |
88 | ///
89 | /// Constant containing a string similar to "ToArray".
90 | ///
91 | public const string ToArrayMethodName = "ToArray";
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/module/PSLambda.psd1:
--------------------------------------------------------------------------------
1 | #
2 | # Module manifest for module 'PSLambda'
3 | #
4 | # Generated by: Patrick M. Meinecke
5 | #
6 | # Generated on: 4/20/2018
7 | #
8 |
9 | @{
10 |
11 | # Script module or binary module file associated with this manifest.
12 | RootModule = 'PSLambda.psm1'
13 |
14 | # Version number of this module.
15 | ModuleVersion = '0.2.0'
16 |
17 | # ID used to uniquely identify this module
18 | GUID = '242ef850-1f6d-4647-acbe-26d010c4a3f5'
19 |
20 | # Author of this module
21 | Author = 'Patrick M. Meinecke'
22 |
23 | # Company or vendor of this module
24 | CompanyName = 'Community'
25 |
26 | # Copyright statement for this module
27 | Copyright = '(c) 2018 Patrick M. Meinecke. All rights reserved.'
28 |
29 | # Description of the functionality provided by this module
30 | Description = 'A runtime delegate compiler for PowerShell ScriptBlock objects.'
31 |
32 | # Minimum version of the Windows PowerShell engine required by this module
33 | PowerShellVersion = '5.1'
34 |
35 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
36 | DotNetFrameworkVersion = '4.7.1'
37 |
38 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
39 | CLRVersion = '4.0'
40 |
41 | # Processor architecture (None, X86, Amd64) required by this module
42 | ProcessorArchitecture = 'None'
43 |
44 | # Format files (.ps1xml) to be loaded when importing this module
45 | FormatsToProcess = 'PSLambda.format.ps1xml'
46 |
47 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
48 | FunctionsToExport = @()
49 |
50 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
51 | CmdletsToExport = 'New-PSDelegate'
52 |
53 | # Variables to export from this module
54 | VariablesToExport = @()
55 |
56 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
57 | AliasesToExport = @()
58 |
59 | # List of all files packaged with this module
60 | FileList = 'PSLambda.psd1',
61 | 'PSLambda.psm1',
62 | 'PSLambda.types.ps1xml',
63 | 'PSLambda.format.ps1xml',
64 | 'PSLambda.dll',
65 | 'PSLambda.pdb',
66 | 'PSLambda.deps.json',
67 | 'PSLambda.xml'
68 |
69 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
70 | PrivateData = @{
71 |
72 | PSData = @{
73 |
74 | # Tags applied to this module. These help with module discovery in online galleries.
75 | Tags = @()
76 |
77 | # A URL to the license for this module.
78 | LicenseUri = 'https://github.com/SeeminglyScience/PSLambda/blob/master/LICENSE'
79 |
80 | # A URL to the main website for this project.
81 | ProjectUri = 'https://github.com/SeeminglyScience/PSLambda'
82 |
83 | # A URL to an icon representing this module.
84 | # IconUri = ''
85 |
86 | # ReleaseNotes of this module
87 | ReleaseNotes = @'
88 | - Add extension method resolution
89 | - Add generic parameter inference
90 | - Fix scriptblocks in method argument binding
91 | - Fix ForEach enumerator typing
92 | - Fix index expressions for non IList types
93 | '@
94 |
95 | } # End of PSData hashtable
96 |
97 | } # End of PrivateData hashtable
98 |
99 | }
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/PSLambda/LabelScopeStack.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 |
6 | namespace PSLambda
7 | {
8 | ///
9 | /// Represents the current set of scopes in which the return keyword will work.
10 | ///
11 | internal class LabelScopeStack
12 | {
13 | private LabelScope _current;
14 |
15 | ///
16 | /// Gets the implied or explicit return type.
17 | ///
18 | public Type ReturnType => _current?.ReturnType;
19 |
20 | ///
21 | /// Creates a new scope in which the return keyword will work.
22 | ///
23 | ///
24 | /// A handle that will return to the previous scope when disposed.
25 | ///
26 | public IDisposable NewScope()
27 | {
28 | _current = _current == null ? new LabelScope() : new LabelScope(_current);
29 | return new ScopeHandle(() => _current = _current?._parent);
30 | }
31 |
32 | ///
33 | /// Creates a new scope in which the return keyword will work while specifying an
34 | /// expected return type.
35 | ///
36 | /// The expected return type.
37 | ///
38 | /// A handle that will return to the previous scope when disposed.
39 | ///
40 | public IDisposable NewScope(Type returnType)
41 | {
42 | _current = _current == null ? new LabelScope() : new LabelScope(_current);
43 | _current.ReturnType = returnType;
44 | return new ScopeHandle(() => _current = _current?._parent);
45 | }
46 |
47 | ///
48 | /// Gets the specified statement adding the return label if applicable.
49 | ///
50 | /// The expressions to precent the label.
51 | ///
52 | /// A value indicating whether an explicit return statement should be required.
53 | ///
54 | ///
55 | /// If a return label is required the supplied expressions will be returned followed
56 | /// by the required label, otherwise they will be returned unchanged.
57 | ///
58 | public Expression[] WithReturn(IEnumerable expressions, bool requireExplicitReturn = true)
59 | {
60 | if (_current.IsReturnRequested)
61 | {
62 | return expressions.Concat(
63 | new[]
64 | {
65 | Expression.Label(
66 | _current.Label,
67 | Expression.Default(_current.ReturnType)),
68 | }).ToArray();
69 | }
70 |
71 | if (!requireExplicitReturn)
72 | {
73 | return expressions.ToArray();
74 | }
75 |
76 | _current.IsReturnRequested = true;
77 | _current.ReturnType = typeof(void);
78 | return expressions.Concat(new[] { Expression.Label(_current.Label) }).ToArray();
79 | }
80 |
81 | ///
82 | /// Gets an existing return label if one has already been defined, otherwise
83 | /// one is created.
84 | ///
85 | /// The expected return type.
86 | /// The requested.
87 | public LabelTarget GetOrCreateReturnLabel(Type type)
88 | {
89 | if (_current != null && _current.IsReturnRequested)
90 | {
91 | return _current.Label;
92 | }
93 |
94 | _current.IsReturnRequested = true;
95 | _current.ReturnType = type;
96 | return _current.Label;
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/test/MethodResolution.Tests.ps1:
--------------------------------------------------------------------------------
1 | using namespace System
2 | using namespace System.Management.Automation
3 | using namespace System.Management.Automation.Language
4 |
5 | $moduleName = 'PSLambda'
6 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
7 |
8 | Import-Module $manifestPath -Force
9 |
10 | Describe 'Method resolution tests' {
11 | It 'can resolve generic parameters from usage' {
12 | $delegate = New-PSDelegate {
13 | $ExecutionContext.InvokeProvider.ChildItem.Get('function:', $true).
14 | Select{ $pso => $pso.BaseObject }.
15 | OfType([g[System.Management.Automation.FunctionInfo]]).
16 | FirstOrDefault{ $f => $f.Name.Equals('TabExpansion2') }
17 | }
18 |
19 | $delegate.Invoke().Name | Should -Be TabExpansion2
20 | }
21 |
22 | It 'can resolve extension methods other than Linq' {
23 | $sb = [scriptblock]::Create('
24 | using namespace System.Threading.Tasks
25 |
26 | New-PSDelegate {
27 | $task = [Task]::FromResult([Task]::Run{ return "testing" })
28 | return $task.Unwrap()
29 | }')
30 |
31 | $delegate = $sb.Invoke()
32 | $delegate.Invoke().GetAwaiter().GetResult() | Should -Be testing
33 | }
34 |
35 | It 'can pass variables by ref' {
36 | $delegate = New-PSDelegate {
37 | [Token[]] $tokens = $null
38 | [ParseError[]] $errors = $null
39 | $ast = [Parser]::ParseInput('{', $tokens, $errors)
40 | return [Tuple]::Create($ast, $tokens, $errors)
41 | }
42 |
43 | $result = $delegate.Invoke()
44 | $result.Item1 | Should -BeOfType System.Management.Automation.Language.Ast
45 | $result.Item2.GetType() | Should -Be ([Token[]])
46 | $result.Item2.Length | Should -Be 2
47 | $result.Item3.GetType() | Should -Be ([ParseError[]])
48 | $result.Item3.Length | Should -Be 1
49 | }
50 |
51 | It 'can convert delegates to other than Func/Action ' {
52 | $delegate = New-PSDelegate {
53 | $list = [System.Collections.Generic.List[int]]::new(0..10)
54 | return $list.ConvertAll{ $i => $i.ToString() }
55 | }
56 |
57 | $delegate.Invoke().GetType() | Should -Be ([System.Collections.Generic.List[string]])
58 | }
59 |
60 | It 'can resolve many generic parameters' {
61 | $delegate = New-PSDelegate {
62 | [Tuple]::Create(
63 | 10,
64 | [Exception]::new(),
65 | 'string',
66 | $Host,
67 | [runspace]::DefaultRunspace,
68 | [ConsoleColor]::Black,
69 | @{}).
70 | ToValueTuple()
71 | }
72 |
73 | $delegate.Invoke().GetType() |
74 | Should -Be (
75 | [ValueTuple[int, exception, string, Host.PSHost, runspace, ConsoleColor, hashtable]])
76 | }
77 |
78 | It 'throws the correct message when a member does not exist' {
79 | $expectedMsg =
80 | "'System.String' does not contain a definition for 'RandomName' " +
81 | "and no extension method 'RandomName' accepting a first argument " +
82 | "of type 'System.String' could be found."
83 |
84 | { New-PSDelegate { ''.RandomName() }} | Should -Throw $expectedMsg
85 | }
86 |
87 | It 'throws the correct message when method arguments do not match' {
88 | $expectedMsg =
89 | "'System.String' does not contain a definition for a method " +
90 | "named 'Compare' that takes the specified arguments."
91 |
92 | { New-PSDelegate { [string]::Compare() }} | Should -Throw $expectedMsg
93 |
94 | $expectedMsg =
95 | "'System.Management.Automation.Host.PSHostUserInterface' does not " +
96 | "contain a definition for a method named 'WriteLine' that takes the " +
97 | "specified arguments."
98 |
99 | { New-PSDelegate { $Host.UI.WriteLine(10) }} | Should -Throw $expectedMsg
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/test/TryCatchFinally.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Import-Module $manifestPath -Force
5 |
6 | Describe 'try/catch/finally tests' {
7 | It 'catch all' {
8 | $delegate = New-PSDelegate {
9 | $didFail = $false
10 | try {
11 | throw
12 | } catch {
13 | $didFail = $true
14 | }
15 |
16 | return $didFail
17 | }
18 |
19 | $delegate.Invoke() | Should -Be $true
20 | }
21 |
22 | It 'catch without matching type throws' {
23 | $delegate = New-PSDelegate {
24 | $didFail = $false
25 | try {
26 | throw
27 | } catch [InvalidOperationException] {
28 | $didFail = $true
29 | }
30 |
31 | return $didFail
32 | }
33 |
34 | { $delegate.Invoke() } | Should -Throw 'ScriptHalted'
35 | }
36 |
37 | It 'catch with filter can does not throw' {
38 | $delegate = New-PSDelegate {
39 | $didFail = $false
40 | try {
41 | throw [InvalidOperationException]::new()
42 | } catch [InvalidOperationException] {
43 | $didFail = $true
44 | }
45 |
46 | return $didFail
47 | }
48 |
49 | $delegate.Invoke() | Should -Be $true
50 | }
51 |
52 | It 'catch with multiple filters' {
53 | $delegate = New-PSDelegate {
54 | $catch = 'none'
55 | try {
56 | throw [InvalidOperationException]::new()
57 | } catch [ArgumentException] {
58 | $catch = 'argument'
59 | } catch [InvalidOperationException] {
60 | $catch = 'invalid operation'
61 | }
62 | catch {
63 | $catch = 'catch all'
64 | }
65 |
66 | return $catch
67 | }
68 |
69 | $delegate.Invoke() | Should -Be 'invalid operation'
70 | }
71 |
72 | It 'finally fires when thrown with no catch' {
73 | $didFail = $false
74 | $delegate = New-PSDelegate {
75 | try {
76 | throw
77 | } finally {
78 | $didFail = $true
79 | }
80 | }
81 |
82 | { $delegate.Invoke() } | Should -Throw ScriptHalted
83 | $didFail | Should -Be $true
84 | }
85 |
86 | It 'finally fires when caught' {
87 | $didFire = $false
88 | $delegate = New-PSDelegate {
89 | try {
90 | throw
91 | } catch {
92 | return
93 | } finally {
94 | $didFire = $true
95 | }
96 | }
97 |
98 | $delegate.Invoke()
99 | $didFire | Should -Be $true
100 | }
101 |
102 | It 'finally fires with no catch block' {
103 | $didFire = $false
104 | $delegate = New-PSDelegate {
105 | try {
106 | return
107 | } finally {
108 | $didFire = $true
109 | }
110 | }
111 |
112 | $delegate.Invoke()
113 | $didFire | Should -Be $true
114 | }
115 |
116 | It 'assigns $_ in catch all' {
117 | $delegate = New-PSDelegate {
118 | try {
119 | throw [InvalidOperationException]::new()
120 | } catch {
121 | return 'caught "{0}"' -f $PSItem.GetType().FullName
122 | }
123 |
124 | return 'failed'
125 | }
126 |
127 | $delegate.Invoke() | Should -Be 'caught "System.InvalidOperationException"'
128 | }
129 |
130 | It 'strongly types $_ in explicitly typed catch' {
131 | $delegate = New-PSDelegate {
132 | try {
133 | throw [System.Management.Automation.RuntimeException]::new()
134 | } catch [System.Management.Automation.RuntimeException] {
135 | return $PSItem.ErrorRecord
136 | }
137 |
138 | return default([System.Management.Automation.ErrorRecord])
139 | }
140 |
141 | $record = $delegate.Invoke()
142 | $record | Should -BeOfType System.Management.Automation.ErrorRecord
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/PSLambda/DelegateTypeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Management.Automation;
5 | using System.Management.Automation.Language;
6 |
7 | namespace PSLambda
8 | {
9 | ///
10 | /// Provides conversion from to any type.
11 | ///
12 | public class DelegateTypeConverter : PSTypeConverter
13 | {
14 | private static readonly Dictionary> s_delegateCache = new Dictionary>();
15 |
16 | private static readonly object s_syncObject = new object();
17 |
18 | ///
19 | /// Determines if a object can be converted to a specific type.
20 | ///
21 | /// The value to convert.
22 | /// The type to conver to.
23 | /// A value indicating whether the object can be converted.
24 | public override bool CanConvertFrom(object sourceValue, Type destinationType)
25 | {
26 | return sourceValue is PSDelegate && typeof(Delegate).IsAssignableFrom(destinationType);
27 | }
28 |
29 | ///
30 | /// Determines if a object can be converted to a specific type.
31 | ///
32 | /// The value to convert.
33 | /// The type to conver to.
34 | /// A value indicating whether the object can be converted.
35 | public override bool CanConvertTo(object sourceValue, Type destinationType)
36 | {
37 | return sourceValue is PSDelegate && typeof(Delegate).IsAssignableFrom(destinationType);
38 | }
39 |
40 | ///
41 | /// Converts a object to a type.
42 | ///
43 | /// The to convert.
44 | ///
45 | /// The type inheriting from to convert to.
46 | ///
47 | /// The parameter is not used.
48 | /// The parameter is not used.
49 | /// The converted object.
50 | public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
51 | {
52 | return ConvertToDelegate((PSDelegate)sourceValue, destinationType);
53 | }
54 |
55 | ///
56 | /// Converts a object to a type.
57 | ///
58 | /// The to convert.
59 | ///
60 | /// The type inheriting from to convert to.
61 | ///
62 | /// The parameter is not used.
63 | /// The parameter is not used.
64 | /// The converted object.
65 | public override object ConvertTo(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
66 | {
67 | return ConvertToDelegate((PSDelegate)sourceValue, destinationType);
68 | }
69 |
70 | private Delegate ConvertToDelegate(PSDelegate psDelegate, Type destinationType)
71 | {
72 | lock (s_syncObject)
73 | {
74 | if (!s_delegateCache.TryGetValue(psDelegate, out Dictionary cacheEntry))
75 | {
76 | cacheEntry = new Dictionary();
77 | s_delegateCache.Add(psDelegate, cacheEntry);
78 | }
79 |
80 | if (!cacheEntry.TryGetValue(destinationType, out Delegate compiledDelegate))
81 | {
82 | compiledDelegate = CompileVisitor.CompileAst(
83 | psDelegate.EngineIntrinsics,
84 | (ScriptBlockAst)psDelegate.ScriptBlock.Ast,
85 | psDelegate.Locals.Values.ToArray(),
86 | destinationType);
87 |
88 | cacheEntry.Add(destinationType, compiledDelegate);
89 | }
90 |
91 | return compiledDelegate;
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/PSLambda/SpecialVariables.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq.Expressions;
4 | using System.Management.Automation;
5 | using System.Management.Automation.Host;
6 |
7 | namespace PSLambda
8 | {
9 | ///
10 | /// Provides information about special variables created by the PowerShell engine.
11 | ///
12 | internal static class SpecialVariables
13 | {
14 | ///
15 | /// Contains the names and types of variables that should be available from any scope.
16 | ///
17 | internal static readonly Dictionary AllScope = new Dictionary(StringComparer.OrdinalIgnoreCase)
18 | {
19 | { "?", typeof(bool) },
20 | { "ExecutionContext", typeof(EngineIntrinsics) },
21 | { "Home", typeof(string) },
22 | { "Host", typeof(PSHost) },
23 | { "PID", typeof(int) },
24 | { "PSCulture", typeof(string) },
25 | { "PSHome", typeof(string) },
26 | { "PSUICulture", typeof(string) },
27 | { "PSVersionTable", typeof(System.Collections.Hashtable) },
28 | { "PSEdition", typeof(string) },
29 | { "ShellId", typeof(string) },
30 | { "MaximumHistoryCount", typeof(int) },
31 | };
32 |
33 | ///
34 | /// Provides the names and constant objects for variables that
35 | /// should are language features like true.
36 | ///
37 | internal static readonly Dictionary Constants = new Dictionary(StringComparer.OrdinalIgnoreCase)
38 | {
39 | { Strings.TrueVariableName, Expression.Constant(true, typeof(bool)) },
40 | { Strings.FalseVariableName, Expression.Constant(false, typeof(bool)) },
41 | { Strings.NullVariableName, Expression.Constant(null) },
42 | };
43 |
44 | ///
45 | /// Provides the names of variables that should be ignored when aquiring local
46 | /// scope variables.
47 | ///
48 | internal static readonly HashSet IgnoreLocal = new HashSet(StringComparer.OrdinalIgnoreCase)
49 | {
50 | "?",
51 | "ExecutionContext",
52 | "Home",
53 | "Host",
54 | "PID",
55 | "PSCulture",
56 | "PSHome",
57 | "PSUICulture",
58 | "PSVersionTable",
59 | "PSEdition",
60 | "ShellId",
61 | "MaximumHistoryCount",
62 | "MyInvocation",
63 | "OFS",
64 | "OutputEncoding",
65 | "VerboseHelpErrors",
66 | "LogEngineHealthEvent",
67 | "LogEngineLifecycleEvent",
68 | "LogCommandHealthEvent",
69 | "LogCommandLifecycleEvent",
70 | "LogProviderHealthEvent",
71 | "LogProviderLifecycleEvent",
72 | "LogSettingsEvent",
73 | "PSLogUserData",
74 | "NestedPromptLevel",
75 | "CurrentlyExecutingCommand",
76 | "PSBoundParameters",
77 | "Matches",
78 | "LASTEXITCODE",
79 | "PSDebugContext",
80 | "StackTrace",
81 | "^",
82 | "$",
83 | "?",
84 | "args",
85 | "input",
86 | "error",
87 | "PSEmailServer",
88 | "PSDefaultParameterValues",
89 | "PSScriptRoot",
90 | "PSCommandPath",
91 | "PSSenderInfo",
92 | "foreach",
93 | "switch",
94 | "PWD",
95 | "null",
96 | "true",
97 | "false",
98 | "PSModuleAutoLoadingPreference",
99 | "IsLinux",
100 | "IsMacOS",
101 | "IsWindows",
102 | "IsCoreCLR",
103 | "DebugPreference",
104 | "WarningPreference",
105 | "ErrorActionPreference",
106 | "InformationPreference",
107 | "ProgressPreference",
108 | "VerbosePreference",
109 | "WhatIfPreference",
110 | "ConfirmPreference",
111 | "ErrorView",
112 | "PSSessionConfigurationName",
113 | "PSSessionApplicationName",
114 | "ExecutionContext",
115 | "Host",
116 | "PID",
117 | "PSCulture",
118 | "PSHOME",
119 | "PSUICulture",
120 | "PSVersionTable",
121 | "PSEdition",
122 | "ShellId",
123 | };
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tools/GetDotNet.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [Parameter()]
4 | [ValidateNotNullOrEmpty()]
5 | [string] $Version = '3.0.100',
6 |
7 | [Parameter()]
8 | [string] $PreviewVersion = 'preview8-013656',
9 |
10 | [Parameter()]
11 | [switch] $Unix
12 | )
13 | begin {
14 | function TestDotNetVersion([System.Management.Automation.CommandInfo] $command) {
15 | $existingVersion, $preview = (& $command --version) -split '-',2
16 | if (-not $existingVersion) {
17 | return $false
18 | }
19 |
20 | $existingVersion = $existingVersion -as [version]
21 | $targetVersion = $Version -as [version]
22 | if ($existingVersion -gt $targetVersion) {
23 | return $true
24 | }
25 |
26 | if ($existingVersion -lt $targetVersion) {
27 | return $false
28 | }
29 |
30 | if ($PreviewVersion) {
31 | if (-not $preview) {
32 | return $true
33 | }
34 |
35 | return $preview -ge $PreviewVersion
36 | } elseif ($preview) {
37 | return $false
38 | }
39 |
40 | return $true
41 | }
42 |
43 | $targetFolder = "$PSScriptRoot/dotnet"
44 | $executableName = 'dotnet.exe'
45 | if ($Unix.IsPresent) {
46 | $executableName = 'dotnet'
47 | }
48 | }
49 | end {
50 | $versionText = $Version
51 | if ($PreviewVersion) {
52 | $versionText = $Version, $PreviewVersion -join '-'
53 | }
54 |
55 | if (($dotnet = Get-Command $executableName -ea 0) -and (TestDotNetVersion $dotnet)) {
56 | return $dotnet
57 | }
58 |
59 | $localAppData = [Environment]::GetFolderPath(
60 | [Environment+SpecialFolder]::LocalApplicationData,
61 | [Environment+SpecialFolderOption]::Create)
62 |
63 | $localAppData = Join-Path $localAppData -ChildPath 'Microsoft/dotnet'
64 | if ($dotnet = Get-Command $localAppData/$executableName -ea 0) {
65 | if (TestDotNetVersion $dotnet) {
66 | return $dotnet
67 | }
68 |
69 | # If dotnet is already installed to local AppData but is not the version we are expecting,
70 | # don't remove it. Instead try to install to the project directory (and check for an
71 | # existing one).
72 | if ($dotnet = Get-Command $targetFolder/$executableName -ea 0) {
73 | if (TestDotNetVersion $dotnet) {
74 | return $dotnet
75 | }
76 |
77 | Write-Host -ForegroundColor Yellow Found dotnet $found but require $Version, replacing...
78 | Remove-Item $targetFolder -Recurse
79 | $dotnet = $null
80 | }
81 | } else {
82 | # The Core SDK isn't already installed to local AppData, so install there.
83 | $targetFolder = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($localAppData)
84 | if (-not (Test-Path $targetFolder)) {
85 | $null = New-Item $targetFolder -ItemType Directory -Force -ErrorAction Stop
86 | }
87 | }
88 |
89 | Write-Host -ForegroundColor Green Downloading dotnet version $Version
90 | try {
91 | $installerPath = $null
92 | if ($Unix.IsPresent) {
93 | $uri = "https://dot.net/v1/dotnet-install.sh"
94 | $installerPath = [System.IO.Path]::GetTempPath() + 'dotnet-install.sh'
95 | $scriptText = [System.Net.WebClient]::new().DownloadString($uri)
96 | Set-Content $installerPath -Value $scriptText -Encoding UTF8
97 | $installer = {
98 | param($Version, $InstallDir)
99 | end {
100 | & (Get-Command bash) $installerPath -Version $Version -InstallDir $InstallDir
101 | }
102 | }
103 | } else {
104 | $uri = "https://dot.net/v1/dotnet-install.ps1"
105 | $scriptText = [System.Net.WebClient]::new().DownloadString($uri)
106 |
107 | # Stop the official script from hard exiting at times...
108 | $safeScriptText = $scriptText -replace 'exit 0', 'return'
109 | $installer = [scriptblock]::Create($safeScriptText)
110 | }
111 |
112 | $null = & $installer -Version $versionText -InstallDir $targetFolder
113 | } finally {
114 | if (-not [string]::IsNullOrEmpty($installerPath) -and (Test-Path $installerPath)) {
115 | Remove-Item $installerPath -ErrorAction Ignore
116 | }
117 | }
118 |
119 | $found = Get-Command $targetFolder/$executableName
120 | if (-not (TestDotNetVersion $found)) {
121 | throw 'The dotnet CLI was downloaded without errors but appears to be the incorrect version.'
122 | }
123 |
124 | return $found
125 | }
126 |
--------------------------------------------------------------------------------
/src/PSLambda/PSDelegate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Management.Automation;
5 | using System.Management.Automation.Language;
6 |
7 | namespace PSLambda
8 | {
9 | ///
10 | /// Represents an that is not yet compiled but can be
11 | /// converted implicitly by the PowerShell engine.
12 | ///
13 | public sealed class PSDelegate
14 | {
15 | private Delegate _defaultDelegate;
16 |
17 | ///
18 | /// Initializes a new instance of the class.
19 | ///
20 | /// The to compile.
21 | public PSDelegate(ScriptBlock scriptBlock)
22 | {
23 | using (var pwsh = PowerShell.Create(RunspaceMode.CurrentRunspace))
24 | {
25 | Locals =
26 | pwsh.AddCommand("Microsoft.PowerShell.Utility\\Get-Variable")
27 | .AddParameter("Scope", 0)
28 | .Invoke()
29 | .Where(v => !SpecialVariables.IgnoreLocal.Contains(v.Name))
30 | .ToDictionary(v => v.Name);
31 |
32 | pwsh.Commands.Clear();
33 |
34 | EngineIntrinsics =
35 | pwsh.AddCommand("Microsoft.PowerShell.Utility\\Get-Variable")
36 | .AddParameter("Name", "ExecutionContext")
37 | .AddParameter("ValueOnly", true)
38 | .Invoke()
39 | .First();
40 | }
41 |
42 | ScriptBlock = scriptBlock;
43 | }
44 |
45 | ///
46 | /// Initializes a new instance of the class.
47 | ///
48 | /// The to compile.
49 | ///
50 | /// The instance for the current runspace.
51 | ///
52 | ///
53 | /// Any objects that are local to the
54 | /// current scope with the exception of AllScope variables.
55 | ///
56 | internal PSDelegate(ScriptBlock scriptBlock, EngineIntrinsics engine, Dictionary locals)
57 | {
58 | Locals = locals;
59 | EngineIntrinsics = engine;
60 | ScriptBlock = scriptBlock;
61 | }
62 |
63 | ///
64 | /// Gets the ScriptBlock represented by the delegate.
65 | ///
66 | public ScriptBlock ScriptBlock { get; }
67 |
68 | ///
69 | /// Gets the from the origin
70 | /// .
71 | ///
72 | internal EngineIntrinsics EngineIntrinsics { get; }
73 |
74 | ///
75 | /// Gets the objects from origin SessionStateScope.
76 | ///
77 | internal Dictionary Locals { get; }
78 |
79 | ///
80 | /// Gets the default that is used for the
81 | /// method.
82 | ///
83 | internal Delegate DefaultDelegate
84 | {
85 | get
86 | {
87 | if (_defaultDelegate != null)
88 | {
89 | return _defaultDelegate;
90 | }
91 |
92 | return _defaultDelegate = CreateDefaultDelegate();
93 | }
94 | }
95 |
96 | ///
97 | /// Invokes the compiled represented by this object. If the
98 | /// has not yet been compiled, it will be compiled prior to
99 | /// invocation.
100 | ///
101 | /// Arguments to pass to the .
102 | /// The result returned by the .
103 | public object Invoke(params object[] arguments)
104 | {
105 | return DefaultDelegate.DynamicInvoke(arguments);
106 | }
107 |
108 | private Delegate CreateDefaultDelegate()
109 | {
110 | return CompileVisitor.CompileAst(
111 | EngineIntrinsics,
112 | (ScriptBlockAst)ScriptBlock.Ast,
113 | Locals.Values.ToArray());
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/PSLambda/ParseErrorWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Management.Automation;
3 | using System.Management.Automation.Language;
4 |
5 | namespace PSLambda
6 | {
7 | ///
8 | /// Provides uniform writing and management of objects.
9 | ///
10 | internal abstract class ParseErrorWriter : IParseErrorWriter
11 | {
12 | ///
13 | /// Initializes a new instance of the class.
14 | ///
15 | protected ParseErrorWriter()
16 | {
17 | }
18 |
19 | ///
20 | /// Creates an instance of the default . This
21 | /// error writer will report full error messages and will throw after a maximum
22 | /// of three errors.
23 | ///
24 | /// The defualt .
25 | public static ParseErrorWriter CreateDefault() => new DefaultParseErrorWriter();
26 |
27 | ///
28 | /// Creates an instance of the null . This
29 | /// error writer will not report any error messages and will throw immediately
30 | /// after the first error has been reported.
31 | ///
32 | /// The null .
33 | public static ParseErrorWriter CreateNull() => new NullParseErrorWriter();
34 |
35 | ///
36 | /// Reports a . Handling of the
37 | /// may defer between implementations.
38 | ///
39 | ///
40 | /// The of the error being reported.
41 | ///
42 | /// The id of the error.
43 | ///
44 | /// The message to display in the
45 | /// when parsing is completed.
46 | ///
47 | public virtual void ReportParseError(IScriptExtent extent, string id, string message)
48 | {
49 | }
50 |
51 | ///
52 | /// Throws if the error limit has been hit. Error limit may vary
53 | /// between implementations.
54 | ///
55 | public virtual void ThrowIfErrorLimitHit()
56 | {
57 | }
58 |
59 | ///
60 | /// Throws if any error has been reported.
61 | ///
62 | public virtual void ThrowIfAnyErrors()
63 | {
64 | }
65 |
66 | private class NullParseErrorWriter : ParseErrorWriter
67 | {
68 | private int _errorCount;
69 |
70 | public override void ReportParseError(IScriptExtent extent, string id, string message)
71 | {
72 | _errorCount++;
73 | throw new ParseException();
74 | }
75 |
76 | public override void ThrowIfAnyErrors()
77 | {
78 | if (_errorCount > 0)
79 | {
80 | throw new ParseException();
81 | }
82 | }
83 |
84 | public override void ThrowIfErrorLimitHit()
85 | {
86 | if (_errorCount > 0)
87 | {
88 | throw new ParseException();
89 | }
90 | }
91 | }
92 |
93 | private class DefaultParseErrorWriter : ParseErrorWriter
94 | {
95 | private const int ErrorLimit = 3;
96 |
97 | private readonly List _errors = new List();
98 |
99 | public override void ReportParseError(IScriptExtent extent, string id, string message)
100 | {
101 | _errors.Add(
102 | new ParseError(
103 | extent,
104 | id,
105 | message));
106 |
107 | ThrowIfErrorLimitHit();
108 | }
109 |
110 | public override void ThrowIfErrorLimitHit()
111 | {
112 | if (_errors.Count < ErrorLimit)
113 | {
114 | return;
115 | }
116 |
117 | throw new ParseException(_errors.ToArray());
118 | }
119 |
120 | public override void ThrowIfAnyErrors()
121 | {
122 | if (_errors.Count < 1)
123 | {
124 | return;
125 | }
126 |
127 | throw new ParseException(_errors.ToArray());
128 | }
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | tools/opencover
5 | tools/dotnet
6 | tools/ResGen
7 | src/PSLambda/gen
8 |
9 | # User-specific files
10 | *.suo
11 | *.user
12 | *.userosscache
13 | *.sln.docstates
14 |
15 | # User-specific files (MonoDevelop/Xamarin Studio)
16 | *.userprefs
17 |
18 | # Build results
19 | [Dd]ebug/
20 | [Dd]ebugPublic/
21 | [Rr]elease/
22 | [Rr]eleases/
23 | x64/
24 | x86/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # DNX
49 | project.lock.json
50 | project.fragment.lock.json
51 | artifacts/
52 |
53 | *_i.c
54 | *_p.c
55 | *_i.h
56 | *.ilk
57 | *.meta
58 | *.obj
59 | *.pch
60 | *.pdb
61 | *.pgc
62 | *.pgd
63 | *.rsp
64 | *.sbr
65 | *.tlb
66 | *.tli
67 | *.tlh
68 | *.tmp
69 | *.tmp_proj
70 | *.log
71 | *.vspscc
72 | *.vssscc
73 | .builds
74 | *.pidb
75 | *.svclog
76 | *.scc
77 |
78 | # Chutzpah Test files
79 | _Chutzpah*
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opendb
86 | *.opensdf
87 | *.sdf
88 | *.cachefile
89 | *.VC.db
90 | *.VC.VC.opendb
91 |
92 | # Visual Studio profiler
93 | *.psess
94 | *.vsp
95 | *.vspx
96 | *.sap
97 |
98 | # TFS 2012 Local Workspace
99 | $tf/
100 |
101 | # Guidance Automation Toolkit
102 | *.gpState
103 |
104 | # ReSharper is a .NET coding add-in
105 | _ReSharper*/
106 | *.[Rr]e[Ss]harper
107 | *.DotSettings.user
108 |
109 | # JustCode is a .NET coding add-in
110 | .JustCode
111 |
112 | # TeamCity is a build add-in
113 | _TeamCity*
114 |
115 | # DotCover is a Code Coverage Tool
116 | *.dotCover
117 |
118 | # NCrunch
119 | _NCrunch_*
120 | .*crunch*.local.xml
121 | nCrunchTemp_*
122 |
123 | # MightyMoose
124 | *.mm.*
125 | AutoTest.Net/
126 |
127 | # Web workbench (sass)
128 | .sass-cache/
129 |
130 | # Installshield output folder
131 | [Ee]xpress/
132 |
133 | # DocProject is a documentation generator add-in
134 | DocProject/buildhelp/
135 | DocProject/Help/*.HxT
136 | DocProject/Help/*.HxC
137 | DocProject/Help/*.hhc
138 | DocProject/Help/*.hhk
139 | DocProject/Help/*.hhp
140 | DocProject/Help/Html2
141 | DocProject/Help/html
142 |
143 | # Click-Once directory
144 | publish/
145 |
146 | # Publish Web Output
147 | *.[Pp]ublish.xml
148 | *.azurePubxml
149 | # TODO: Comment the next line if you want to checkin your web deploy settings
150 | # but database connection strings (with potential passwords) will be unencrypted
151 | #*.pubxml
152 | *.publishproj
153 |
154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
155 | # checkin your Azure Web App publish settings, but sensitive information contained
156 | # in these scripts will be unencrypted
157 | PublishScripts/
158 |
159 | # NuGet Packages
160 | *.nupkg
161 | # The packages folder can be ignored because of Package Restore
162 | **/packages/*
163 | # except build/, which is used as an MSBuild target.
164 | !**/packages/build/
165 | # Uncomment if necessary however generally it will be regenerated when needed
166 | #!**/packages/repositories.config
167 | # NuGet v3's project.json files produces more ignoreable files
168 | *.nuget.props
169 | *.nuget.targets
170 |
171 | # Microsoft Azure Build Output
172 | csx/
173 | *.build.csdef
174 |
175 | # Microsoft Azure Emulator
176 | ecf/
177 | rcf/
178 |
179 | # Windows Store app package directories and files
180 | AppPackages/
181 | BundleArtifacts/
182 | Package.StoreAssociation.xml
183 | _pkginfo.txt
184 |
185 | # Visual Studio cache files
186 | # files ending in .cache can be ignored
187 | *.[Cc]ache
188 | # but keep track of directories ending in .cache
189 | !*.[Cc]ache/
190 |
191 | # Others
192 | ClientBin/
193 | ~$*
194 | *~
195 | *.dbmdl
196 | *.dbproj.schemaview
197 | *.jfm
198 | *.pfx
199 | *.publishsettings
200 | node_modules/
201 | orleans.codegen.cs
202 |
203 | # Since there are multiple workflows, uncomment next line to ignore bower_components
204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
205 | #bower_components/
206 |
207 | # RIA/Silverlight projects
208 | Generated_Code/
209 |
210 | # Backup & report files from converting an old project file
211 | # to a newer Visual Studio version. Backup files are not needed,
212 | # because we have git ;-)
213 | _UpgradeReport_Files/
214 | Backup*/
215 | UpgradeLog*.XML
216 | UpgradeLog*.htm
217 |
218 | # SQL Server files
219 | *.mdf
220 | *.ldf
221 |
222 | # Business Intelligence projects
223 | *.rdl.data
224 | *.bim.layout
225 | *.bim_*.settings
226 |
227 | # Microsoft Fakes
228 | FakesAssemblies/
229 |
230 | # GhostDoc plugin setting file
231 | *.GhostDoc.xml
232 |
233 | # Node.js Tools for Visual Studio
234 | .ntvs_analysis.dat
235 |
236 | # Visual Studio 6 build log
237 | *.plg
238 |
239 | # Visual Studio 6 workspace options file
240 | *.opt
241 |
242 | # Visual Studio LightSwitch build output
243 | **/*.HTMLClient/GeneratedArtifacts
244 | **/*.DesktopClient/GeneratedArtifacts
245 | **/*.DesktopClient/ModelManifest.xml
246 | **/*.Server/GeneratedArtifacts
247 | **/*.Server/ModelManifest.xml
248 | _Pvt_Extensions
249 |
250 | # Paket dependency manager
251 | .paket/paket.exe
252 | paket-files/
253 |
254 | # FAKE - F# Make
255 | .fake/
256 |
257 | # JetBrains Rider
258 | .idea/
259 | *.sln.iml
260 |
261 | # CodeRush
262 | .cr/
263 |
264 | # Python Tools for Visual Studio (PTVS)
265 | __pycache__/
266 | *.pyc
267 |
--------------------------------------------------------------------------------
/src/PSLambda/ParseWriterExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Management.Automation.Language;
4 |
5 | namespace PSLambda
6 | {
7 | ///
8 | /// Provides utility methods for reporting specific types of
9 | /// objects.
10 | ///
11 | internal static class ParseWriterExtensions
12 | {
13 | ///
14 | /// Generate a citing an unsupported type.
15 | ///
16 | /// The that should issue the report.
17 | /// The that is not supported.
18 | internal static void ReportNotSupportedAstType(
19 | this IParseErrorWriter writer,
20 | Ast ast)
21 | {
22 | writer.ReportNotSupported(
23 | ast.Extent,
24 | ast.GetType().Name,
25 | string.Format(
26 | CultureInfo.CurrentCulture,
27 | ErrorStrings.AstNotSupported,
28 | ast.GetType().Name));
29 | }
30 |
31 | ///
32 | /// Generate a citing an unsupported element.
33 | ///
34 | /// The that should issue the report.
35 | /// The of the unsupported element.
36 | /// The ID to be shown in the .
37 | /// The message to be shown in the .
38 | internal static void ReportNotSupported(
39 | this IParseErrorWriter writer,
40 | IScriptExtent extent,
41 | string id,
42 | string message)
43 | {
44 | writer.ReportParseError(
45 | extent,
46 | string.Format(
47 | CultureInfo.CurrentCulture,
48 | "{0}.{1}",
49 | nameof(ErrorStrings.ElementNotSupported),
50 | id),
51 | string.Format(
52 | CultureInfo.CurrentCulture,
53 | ErrorStrings.ElementNotSupported,
54 | message));
55 | }
56 |
57 | ///
58 | /// Generate a citing a missing element.
59 | ///
60 | /// The that should issue the report.
61 | /// The of the missing element.
62 | /// The ID to be shown in the .
63 | /// The message to be shown in the .
64 | internal static void ReportMissing(
65 | this IParseErrorWriter writer,
66 | IScriptExtent extent,
67 | string id,
68 | string message)
69 | {
70 | writer.ReportParseError(
71 | extent,
72 | string.Format(
73 | CultureInfo.CurrentCulture,
74 | "{0}.{1}",
75 | nameof(ErrorStrings.ElementMissing),
76 | id),
77 | string.Format(
78 | CultureInfo.CurrentCulture,
79 | ErrorStrings.ElementMissing,
80 | message));
81 | }
82 |
83 | ///
84 | /// Generate a .
85 | ///
86 | /// The that should issue the report.
87 | /// The of element in error.
88 | /// The ID to be shown in the .
89 | /// The message to be shown in the .
90 | internal static void ReportParseError(
91 | this IParseErrorWriter writer,
92 | IScriptExtent extent,
93 | string id = nameof(ErrorStrings.CompileTimeParseError),
94 | string message = "")
95 | {
96 | if (string.IsNullOrWhiteSpace(message))
97 | {
98 | message = ErrorStrings.CompileTimeParseError;
99 | }
100 |
101 | writer.ReportParseError(extent, id, message);
102 | }
103 |
104 | ///
105 | /// Generate a .
106 | ///
107 | /// The that should issue the report.
108 | /// The of element in error.
109 | ///
110 | /// The exception housing the message to be shown in the .
111 | ///
112 | /// The ID to be shown in the .
113 | internal static void ReportParseError(
114 | this IParseErrorWriter writer,
115 | IScriptExtent extent,
116 | Exception exception,
117 | string id = "")
118 | {
119 | if (string.IsNullOrEmpty(id))
120 | {
121 | id = exception.GetType().Name;
122 | }
123 |
124 | writer.ReportParseError(
125 | extent,
126 | id,
127 | exception.Message);
128 | }
129 |
130 | ///
131 | /// Generate a .
132 | ///
133 | /// The that should issue the report.
134 | /// The of element in error.
135 | internal static void ReportNonConstantTypeAs(
136 | this IParseErrorWriter writer,
137 | IScriptExtent extent)
138 | {
139 | writer.ReportNotSupported(
140 | extent,
141 | nameof(ErrorStrings.NonConstantTypeAs),
142 | ErrorStrings.NonConstantTypeAs);
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/PSLambda/resources/CompilerStrings.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | ScriptHalted
122 |
123 |
124 | Begin Block
125 |
126 |
127 | Process Block
128 |
129 |
130 | End Block
131 |
132 |
133 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | We welcome many kinds of community contributions to this project! Whether it's a feature implementation,
4 | bug fix, or a good idea, please create an issue so that we can discuss it. It is not necessary to create an
5 | issue before sending a pull request but it may speed up the process if we can discuss your idea before
6 | you start implementing it.
7 |
8 | Because this project exposes a couple different public APIs, we must be very mindful of any potential breaking
9 | changes. Some contributions may not be accepted if they risk introducing breaking changes or if they
10 | don't match the goals of the project. The core maintainer team has the right of final approval over
11 | any contribution to this project. However, we are very happy to hear community feedback on any decision
12 | so that we can ensure we are solving the right problems in the right way.
13 |
14 | ## Ways to Contribute
15 |
16 | - File a bug or feature request as an [issue](https://github.com/SeeminglyScience/PSLambda/issues)
17 | - Comment on existing issues to give your feedback on how they should be fixed/implemented
18 | - Contribute a bug fix or feature implementation by submitting a pull request
19 | - Contribute more unit tests for feature areas that lack good coverage
20 | - Review the pull requests that others submit to ensure they follow [established guidelines](#pull-request-guidelines)
21 |
22 | ## Code Contribution Guidelines
23 |
24 | Here's a high level list of guidelines to follow to ensure your code contribution is accepted:
25 |
26 | - Follow established guidelines for coding style and design
27 | - Follow established guidelines for commit hygiene
28 | - Write unit tests to validate new features and bug fixes
29 | - Ensure that the 'Release' build and unit tests pass locally
30 | - Respond to all review feedback and final commit cleanup
31 |
32 | ### Practice Good Commit Hygiene
33 |
34 | First of all, make sure you are practicing [good commit hygiene](http://blog.ericbmerritt.com/2011/09/21/commit-hygiene-and-git.html)
35 | so that your commits provide a good history of the changes you are making. To be more specific:
36 |
37 | - **Write good commit messages**
38 |
39 | Commit messages should be clearly written so that a person can look at the commit log and understand
40 | how and why a given change was made. Here is a great model commit message taken from a [blog post
41 | by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html):
42 |
43 | Capitalized, short (50 chars or less) summary
44 |
45 | More detailed explanatory text, if necessary. Wrap it to about 72
46 | characters or so. In some contexts, the first line is treated as the
47 | subject of an email and the rest of the text as the body. The blank
48 | line separating the summary from the body is critical (unless you omit
49 | the body entirely); tools like rebase can get confused if you run the
50 | two together.
51 |
52 | Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
53 | or "Fixes bug." This convention matches up with commit messages generated
54 | by commands like git merge and git revert.
55 |
56 | Further paragraphs come after blank lines.
57 |
58 | - Bullet points are okay, too
59 |
60 | - Typically a hyphen or asterisk is used for the bullet, followed by a
61 | single space, with blank lines in between, but conventions vary here
62 |
63 | - Use a hanging indent
64 |
65 | A change that fixes a known bug with an issue filed should use the proper syntax so that the [issue
66 | is automatically closed](https://help.github.com/articles/closing-issues-via-commit-messages/) once
67 | your change is merged. Here's an example of what such a commit message should look like:
68 |
69 | Fix #3: Catch NullReferenceException from DoThing
70 |
71 | This change adds a try/catch block to catch a NullReferenceException that
72 | gets thrown by DoThing [...]
73 |
74 | - **Squash your commits**
75 |
76 | If you are introducing a new feature but have implemented it over multiple commits,
77 | please [squash those commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
78 | into a single commit that contains all the changes in one place. This especially applies to any "oops"
79 | commits where a file is forgotten or a typo is being fixed. Following this approach makes it a lot easier
80 | to pull those changes to other branches or roll back the change if necessary.
81 |
82 | - **Keep individual commits for larger changes**
83 |
84 | You can certainly maintain individual commits for different phases of a big change. For example, if
85 | you want to reorganize some files before adding new functionality, have your first commit contain all
86 | of the file move changes and then the following commit can have all of the feature additions. We
87 | highly recommend this approach so that larger commits don't turn into a jumbled mess.
88 |
89 | ### Build 'Release' Before Submitting
90 |
91 | Before you send out your pull request, make sure that you have run a Release configuration build of the
92 | project and that all new and existing tests are passing. The Release configuration build ensures that
93 | all public API interfaces have been documented correctly otherwise it throws an error. We have turned
94 | on this check so that our project will always have good generated documentation.
95 |
96 | ### Follow the Pull Request Process
97 |
98 | - **Create your pull request**
99 |
100 | Use the [typical process](https://help.github.com/articles/using-pull-requests/) to send a pull request
101 | from your fork of the project. In your pull request message, please give a high-level summary of the
102 | changes that you have made so that reviewers understand the intent of the changes. You should receive
103 | initial comments within a day or two, but please feel free to ping if things are taking longer than
104 | expected.
105 |
106 | - **Respond to code review feedback**
107 |
108 | If the reviewers ask you to make changes, make them as a new commit to your branch and push them so
109 | that they are made available for a final review pass. Do not rebase the fixes just yet so that the
110 | commit hash changes don't upset GitHub's pull request UI.
111 |
112 | - **If necessary, do a final rebase**
113 |
114 | Once your final changes have been accepted, we may ask you to do a final rebase to have your commits
115 | so that they follow our commit guidelines. If specific guidance is given, please follow it when
116 | rebasing your commits. Once you do your final push, we will merge your changes!
117 |
--------------------------------------------------------------------------------
/test/Loops.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Import-Module $manifestPath -Force
5 |
6 | Describe 'basic loop functionality' {
7 | Context 'foreach statement tests' {
8 | It 'can enumerate IEnumerable<>' {
9 | $delegate = New-PSDelegate {
10 | $total = 0
11 | foreach($item in 0..10) {
12 | $total = $item + $total
13 | }
14 |
15 | return $total
16 | }
17 |
18 | $delegate.Invoke() | Should -Be 55
19 | }
20 |
21 | It 'can enumerate IDictionary' {
22 | $delegate = New-PSDelegate {
23 | $hashtable = @{
24 | one = 'two'
25 | three = 'four'
26 | }
27 |
28 | $results = [System.Collections.Generic.List[string]]::new()
29 | foreach($item in $hashtable) {
30 | $results.Add($item.Value.ToString())
31 | }
32 |
33 | return $results
34 | }
35 |
36 | $results = $delegate.Invoke()
37 | $results | Should -Contain two
38 | $results | Should -Contain four
39 | }
40 |
41 | It 'prioritizes IEnumerable<> over IDictionary' {
42 | $delegate = New-PSDelegate {
43 | $map = [System.Collections.Generic.Dictionary[string, int]]::new()
44 | $map.Add('test', 10)
45 | $map.Add('test2', 30)
46 |
47 | $results = [System.Collections.Generic.List[int]]::new()
48 | foreach ($item in $map) {
49 | $results.Add($item.Value)
50 | }
51 |
52 | return $results
53 | }
54 |
55 | $delegate.Invoke() | Should -Be 10, 30
56 | }
57 |
58 | It 'can enumerable IEnumerable' {
59 | $delegate = New-PSDelegate {
60 | $list = [System.Collections.ArrayList]::new()
61 | $list.Add([object]10)
62 | $list.Add('test2')
63 |
64 | $results = [System.Collections.Generic.List[string]]::new()
65 | foreach ($item in $list) {
66 | $results.Add($item.ToString())
67 | }
68 |
69 | return $results
70 | }
71 |
72 | $delegate.Invoke() | Should -Be 10, test2
73 | }
74 |
75 | It 'throws the correct message when target is not IEnumerable' {
76 | $expectedMsg =
77 | "The foreach statement cannot operate on variables of type " +
78 | "'System.Int32' because 'System.Int32' does not contain a " +
79 | "public definition for 'GetEnumerator'"
80 |
81 | { New-PSDelegate { foreach ($a in 10) {}}} | Should -Throw $expectedMsg
82 | }
83 | }
84 |
85 | It 'for statement' {
86 | $delegate = New-PSDelegate {
87 | [int] $total = 0
88 | for ([int] $i = 0; $i -lt 10; $i++) {
89 | $total = $i + $total
90 | }
91 |
92 | return $total
93 | }
94 |
95 | $delegate.Invoke() | Should -Be 45
96 | }
97 |
98 | It 'while statement' {
99 | $delegate = New-PSDelegate {
100 | [int] $i = 0
101 | while ($i -lt 10) {
102 | $i++
103 | }
104 |
105 | return $i
106 | }
107 |
108 | $delegate.Invoke() | Should -Be 10
109 | }
110 |
111 | It 'do while statement' {
112 | $delegate = New-PSDelegate {
113 | [int] $i = 0
114 | do {
115 | $i++
116 | } while ($i -lt 10)
117 |
118 | return $i
119 | }
120 |
121 | $delegate.Invoke() | Should -Be 10
122 | }
123 |
124 | It 'do until statement' {
125 | $delegate = New-PSDelegate {
126 | [int] $i = 0
127 | do {
128 | $i++
129 | } until ($i -gt 10)
130 |
131 | return $i
132 | }
133 |
134 | $delegate.Invoke() | Should -Be 11
135 | }
136 |
137 | It 'break continues after loop' {
138 | $delegate = New-PSDelegate {
139 | $i = 0
140 | $continuedAfterBreak = $false
141 | while ($i -lt 10) {
142 | $i++
143 | if ($i -eq 5) {
144 | break
145 | $continuedAfterBreak = $true
146 | }
147 | }
148 |
149 | if ($continuedAfterBreak) {
150 | throw 'code after "break" was executed'
151 | }
152 |
153 | return $i
154 | }
155 |
156 | $delegate.Invoke() | Should -Be 5
157 | }
158 |
159 | It 'continue steps to next interation' {
160 | $delegate = New-PSDelegate {
161 | $i = 0
162 | $continuedAfterContinue = $false
163 | while ($i -lt 10) {
164 | $i++
165 | continue
166 | $continuedAfterContinue = $true
167 | }
168 |
169 | if ($continuedAfterContinue) {
170 | throw 'code after "continue" was executed'
171 | }
172 |
173 | return $i
174 | }
175 |
176 | $delegate.Invoke() | Should -Be 10
177 | }
178 |
179 | Context 'switch statement' {
180 | It 'chooses correct value' {
181 | $hitValues = [System.Collections.Generic.List[string]]::new()
182 | $delegate = New-PSDelegate {
183 | foreach ($value in 'value1', 'value2', 'value3', 'invalid') {
184 | switch ($value) {
185 | value1 { $hitValues.Add('option1') }
186 | value2 { $hitValues.Add('option2') }
187 | value3 { $hitValues.Add('option3') }
188 | default { throw }
189 | }
190 | }
191 | }
192 |
193 | { $delegate.Invoke() } | Should -Throw
194 | $hitValues | Should -Be 'option1', 'option2', 'option3'
195 | }
196 |
197 | It 'can have a single value' {
198 | $delegate = New-PSDelegate {
199 | switch ('value') {
200 | value { return 'value' }
201 | }
202 | }
203 |
204 | $delegate.Invoke() | Should -Be 'value'
205 | }
206 |
207 | It 'can have a default without cases' {
208 | $delegate = New-PSDelegate {
209 | switch ('value') {
210 | default { return 'value' }
211 | }
212 | }
213 |
214 | $delegate.Invoke() | Should -Be 'value'
215 | }
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/PSLambda/VariableScopeStack.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Linq.Expressions;
4 | using System.Management.Automation.Language;
5 |
6 | namespace PSLambda
7 | {
8 | ///
9 | /// Represents the current set of variable scopes for a block objects.
10 | ///
11 | internal class VariableScopeStack
12 | {
13 | private static readonly ParameterExpression[] s_emptyParameters = new ParameterExpression[0];
14 |
15 | private VariableScope _current;
16 |
17 | ///
18 | /// Initializes a new instance of the class.
19 | ///
20 | internal VariableScopeStack()
21 | {
22 | _current = new VariableScope(s_emptyParameters);
23 | }
24 |
25 | ///
26 | /// Creates a new variable scope.
27 | ///
28 | ///
29 | /// A handle that will return to the previous scope when disposed.
30 | ///
31 | internal IDisposable NewScope()
32 | {
33 | _current = new VariableScope(_current);
34 | return new ScopeHandle(() => _current = _current?.Parent);
35 | }
36 |
37 | ///
38 | /// Creates a new variable scope.
39 | ///
40 | /// Parameters that should be available from the new scope.
41 | ///
42 | /// A handle that will return to the previous scope when disposed.
43 | ///
44 | internal IDisposable NewScope(ParameterExpression[] parameters)
45 | {
46 | var handle = NewScope();
47 | var currentScope = _current;
48 | foreach (var parameter in parameters)
49 | {
50 | currentScope.Parameters.Add(parameter.Name, parameter);
51 | }
52 |
53 | return handle;
54 | }
55 |
56 | ///
57 | /// Creates a new variable expression without looking at previous scopes.
58 | ///
59 | /// The name of the variable.
60 | /// The type of the variable.
61 | /// The variable .
62 | internal ParameterExpression NewVariable(string name, Type type)
63 | {
64 | return _current.NewVariable(name, type);
65 | }
66 |
67 | ///
68 | /// Gets a variable from the current or a parent scope.
69 | ///
70 | ///
71 | /// The to obtain a variable for.
72 | ///
73 | ///
74 | /// A value indicating whether the variable has already been defined.
75 | ///
76 | /// The resolved variable .
77 | internal ParameterExpression GetVariable(
78 | VariableExpressionAst variableExpressionAst,
79 | out bool alreadyDefined)
80 | {
81 | return _current.GetOrCreateVariable(variableExpressionAst.VariablePath.UserPath, variableExpressionAst.StaticType, out alreadyDefined);
82 | }
83 |
84 | ///
85 | /// Gets a variable from the current or a parent scope.
86 | ///
87 | /// The name of the variable.
88 | /// The type of the variable.
89 | /// The resolved variable .
90 | internal ParameterExpression GetVariable(string name, Type type)
91 | {
92 | return _current.GetOrCreateVariable(name, type);
93 | }
94 |
95 | ///
96 | /// Gets all variables declared in the current scope.
97 | ///
98 | ///
99 | /// The variable objects declared in the current scope.
100 | ///
101 | internal ParameterExpression[] GetVariables()
102 | {
103 | return _current.Variables.Values.ToArray();
104 | }
105 |
106 | ///
107 | /// Gets all parameters declared in the current scope.
108 | ///
109 | ///
110 | /// The parameter objects declared in the current scope.
111 | ///
112 | internal ParameterExpression[] GetParameters()
113 | {
114 | return _current.Parameters.Values.ToArray();
115 | }
116 |
117 | ///
118 | /// Gets the automatic variable $_ or $PSItem from the
119 | /// current scope, or closest parent scope where it is defined.
120 | ///
121 | ///
122 | /// The parameter expression referencing dollar under if defined;
123 | /// otherwise .
124 | ///
125 | ///
126 | /// if a defined dollar under variable was
127 | /// found; otherwise .
128 | ///
129 | internal bool TryGetDollarUnder(out ParameterExpression dollarUnder)
130 | {
131 | dollarUnder = GetDollarUnder();
132 | return dollarUnder != null;
133 | }
134 |
135 | ///
136 | /// Gets the automatic variable $_ or $PSItem from the
137 | /// current scope, or closest parent scope where it is defined.
138 | ///
139 | ///
140 | /// The parameter expression referencing dollar under if defined;
141 | /// otherwise .
142 | ///
143 | internal ParameterExpression GetDollarUnder() => _current.GetDollarUnder();
144 |
145 | ///
146 | /// Sets the automatic variable $_ or $PSItem in the current
147 | /// scope. If the variable exists in a parent scope and the type is the same,
148 | /// then the parent variable will be used. If the variable exists in a parent
149 | /// scope but the type is not the same, the variable will be renamed behind
150 | /// the scenes.
151 | ///
152 | ///
153 | /// The static type that dollar under should contain.
154 | ///
155 | ///
156 | /// The parameter expression referencing dollar under.
157 | ///
158 | internal ParameterExpression SetDollarUnder(Type type) => _current.SetDollarUnder(type);
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/test/MiscLanguageFeatures.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleName = 'PSLambda'
2 | $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
3 |
4 | Import-Module $manifestPath -Force
5 |
6 | Describe 'Misc Language Features' {
7 | It 'expandable string expression' {
8 | $delegate = New-PSDelegate {
9 | $one = "1"
10 | $two = 2
11 | $three = 'something'
12 | return "This is a string with $one random $two numbers and $three"
13 | }
14 |
15 | $delegate.Invoke() | Should -Be 'This is a string with 1 random 2 numbers and something'
16 | }
17 |
18 | Context 'hashtable tests' {
19 | It 'handles varied types of values' {
20 | $delegate = New-PSDelegate {
21 | return @{
22 | 'string' = 'value'
23 | string2 = 10
24 | Object = [object]::new()
25 | }
26 | }
27 |
28 | $hashtable = $delegate.Invoke()
29 | $hashtable['string'] | Should -Be 'value'
30 | $hashtable['string2'] | Should -Be 10
31 | $hashtable['Object'] | Should -BeOfType object
32 | $hashtable['object'] | Should -BeOfType object
33 | }
34 |
35 | It 'can initialize an empty hashtable' {
36 | (New-PSDelegate { @{} }).Invoke().GetType() | Should -Be ([hashtable])
37 | }
38 | }
39 |
40 | Context 'array literal' {
41 | It 'int' {
42 | $result = (New-PSDelegate { 1, 2, 3 }).Invoke()
43 | $result.GetType() | Should -Be ([int[]])
44 | $result | Should -Be 1, 2, 3
45 | }
46 |
47 | It 'string' {
48 | $result = (New-PSDelegate { 'one', 'two', 'three' }).Invoke()
49 | $result.GetType() | Should -Be ([string[]])
50 | $result | Should -Be one, two, three
51 | }
52 |
53 | It 'bool' {
54 | $result = (New-PSDelegate { $true, $false, $true }).Invoke()
55 | $result.GetType() | Should -Be ([bool[]])
56 | $result | Should -Be $true, $false, $true
57 | }
58 |
59 | It 'type' {
60 | $result = (New-PSDelegate { [type], [string], [int] }).Invoke()
61 | $result.GetType() | Should -Be ([type[]])
62 | $result | Should -Be ([type], [string], [int])
63 | }
64 | }
65 |
66 | Context 'array initializer' {
67 | It 'does not double arrays' {
68 | $result = (New-PSDelegate { @(0, 1) }).Invoke()
69 | $result.GetType() | Should -Be ([int[]])
70 | $result[0].GetType() | Should -Be ([int])
71 | $result | Should -Be 0, 1
72 | }
73 |
74 | It 'does create an array for single items' {
75 | $result = (New-PSDelegate { @(1) }).Invoke()
76 | $result.GetType() | Should -Be ([int[]])
77 | $result[0].GetType() | Should -Be ([int])
78 | $result | Should -Be 1
79 | }
80 |
81 | It 'creates an empty array' {
82 | $result = (New-PSDelegate { @() }).Invoke()
83 | $result.GetType() | Should -Be ([object[]])
84 | $result.Length | Should -Be 0
85 | }
86 |
87 | It 'can take multiple statements in an array' {
88 | $delegate = New-PSDelegate {
89 | return @(
90 | 0..10
91 | 10..20)
92 | }
93 |
94 | $result = $delegate.Invoke()
95 | $result.GetType() | Should -Be ([int[][]])
96 | $result.Count | Should -Be 2
97 | $result[0] | Should -Be (0..10)
98 | $result[1] | Should -Be (10..20)
99 | }
100 | }
101 |
102 | Context 'assignments' {
103 | It 'can infer type of rhs' {
104 | $result = (New-PSDelegate { $a = 10; return $a++ }).Invoke()
105 | $result | Should -BeOfType int
106 | $result | Should -Be 11
107 | }
108 |
109 | It 'can access variables assigned from parent scopes' {
110 | $result = (New-PSDelegate {
111 | $a = 10
112 | if ($true) {
113 | $a += 1
114 | }
115 |
116 | return $a
117 | }).Invoke()
118 |
119 | $result | Should -Be 11
120 | }
121 |
122 | It 'can not access a variable declared in a child scope' {
123 | { (New-PSDelegate { if ($true) { $a = 10 }; return $a }).Invoke() } |
124 | Should -Throw 'The variable "a" was referenced before it was defined or was defined in a sibling scope'
125 | }
126 |
127 | It 'can assign to an index operation' {
128 | $delegate = New-PSDelegate {
129 | $hash = @{ Key = 'Value' }
130 | $hash['Key'] = 'NewValue'
131 | return $hash
132 | }
133 |
134 | $delegate.Invoke().Key | Should -Be 'NewValue'
135 | }
136 |
137 | It 'can assign to a property' {
138 | $delegate = New-PSDelegate {
139 | $verboseRecord = [System.Management.Automation.VerboseRecord]::new('original message')
140 | $verboseRecord.Message = 'new message'
141 | return $verboseRecord
142 | }
143 |
144 | $delegate.Invoke().Message | Should -Be 'new message'
145 | }
146 |
147 | It 'minus equals' {
148 | $delegate = New-PSDelegate {
149 | $a = 10
150 | $a -= 5
151 | return $a
152 | }
153 |
154 | $delegate.Invoke() | Should -Be 5
155 | }
156 |
157 | It 'multiply equals' {
158 | $delegate = New-PSDelegate {
159 | $a = 10
160 | $a *= 5
161 | return $a
162 | }
163 |
164 | $delegate.Invoke() | Should -Be 50
165 | }
166 |
167 | It 'divide equals' {
168 | $delegate = New-PSDelegate {
169 | $a = 10
170 | $a /= 5
171 | return $a
172 | }
173 |
174 | $delegate.Invoke() | Should -Be 2
175 | }
176 |
177 | It 'remainder equals' {
178 | $delegate = New-PSDelegate {
179 | $a = 10
180 | $a %= 6
181 | return $a
182 | }
183 |
184 | $delegate.Invoke() | Should -Be 4
185 | }
186 | }
187 |
188 | Context 'indexer inference' {
189 | It 'indexed IList<> are typed property' {
190 | $delegate = New-PSDelegate {
191 | $list = [System.Collections.Generic.List[string]]::new()
192 | $list.Add('test')
193 | return $list[0].EndsWith('t')
194 | }
195 |
196 | $delegate.Invoke() | Should -Be $true
197 | }
198 |
199 | It 'indexed IDictionary<,> are typed property' {
200 | $delegate = New-PSDelegate {
201 | $list = [System.Collections.Generic.Dictionary[string, type]]::new()
202 | $list.Add('test', [type])
203 | return $list['test'].Namespace
204 | }
205 |
206 | $delegate.Invoke() | Should -Be 'System'
207 | }
208 |
209 | It 'can index IList' {
210 | $delegate = New-PSDelegate {
211 | $list = [System.Collections.ArrayList]::new()
212 | $list.AddRange([object[]]('one', 'two', 'three'))
213 | return [string]$list[1] -eq 'two'
214 | }
215 |
216 | $delegate.Invoke() | Should -Be $true
217 | }
218 |
219 | It 'can index custom indexers' {
220 | $delegate = New-PSDelegate {
221 | $pso = [psobject]::AsPSObject([System.Text.StringBuilder]::new())
222 | $pso.Methods['Append'].Invoke(@('testing'))
223 | return $pso
224 | }
225 |
226 | $result = $delegate.Invoke()
227 | $result | Should -BeOfType System.Text.StringBuilder
228 | $result.ToString() | Should -Be testing
229 | }
230 |
231 | It 'throws the correct message when indexer cannot be found' {
232 | $expectedMsg = 'The indexer could not be determined for the type "System.Int32".'
233 | { New-PSDelegate { (10)[0] }} | Should -Throw $expectedMsg
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/PSLambda/resources/ErrorStrings.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | Unable to compile ScriptBlock due to unexpected expression. The specified element must be a constant.
122 |
123 |
124 | Unable to resolve the type "{0}".
125 |
126 |
127 | Expected element to be a type literal expression.
128 |
129 |
130 | -as or -is expression with non-literal type.
131 |
132 |
133 | Unable to compile ScriptBlock.
134 |
135 |
136 | Unable to compile ScriptBlock due to a missing element: "{0}".
137 |
138 |
139 | Unable to compile ScriptBlock due to an unsupported element: "{0}".
140 |
141 |
142 | AST of type "{0}".
143 |
144 |
145 | The variable "{0}" was referenced before it was defined or was defined in a sibling scope.
146 |
147 |
148 | '{0}' is not a member of type '{1}'.
149 |
150 |
151 | The indexer could not be determined for the type "{0}".
152 |
153 |
154 | Multiple pipeline elements
155 |
156 |
157 | Invalid syntax for generic method invocation. The syntax is "generic($Target.Method(arguments), [type], [type])".
158 |
159 |
160 | The {0} keyword requires the third argument to be a ScriptBlock expression.
161 |
162 |
163 | The {0} keyword requires both a target expression and a ScriptBlock expression as arguments.
164 |
165 |
166 | '{0}' does not contain a definition for '{1}' and no extension method '{1}' accepting a first argument of type '{0}' could be found.
167 |
168 |
169 | '{0}' does not contain a definition for a method named '{1}' that takes the specified arguments.
170 |
171 |
172 | The foreach statement cannot operate on variables of type '{0}' because '{0}' does not contain a public definition for 'GetEnumerator'
173 |
174 |
175 |
--------------------------------------------------------------------------------
/PSLambda.build.ps1:
--------------------------------------------------------------------------------
1 | #requires -Module InvokeBuild -Version 5.1
2 |
3 | [CmdletBinding()]
4 | param(
5 | [Parameter()]
6 | [ValidateSet('Debug', 'Release')]
7 | [string] $Configuration = 'Debug',
8 |
9 | [Parameter()]
10 | [string] $Framework = 'netstandard2.0',
11 |
12 | [Parameter()]
13 | [switch] $GenerateCodeCoverage,
14 |
15 | [Parameter()]
16 | [switch] $Force
17 | )
18 |
19 | $ModuleName = 'PSLambda'
20 | $Culture = 'en-US'
21 | $ShouldAnalyze = $false
22 | $ShouldTest = $true
23 |
24 | $FailOnError = @{
25 | ErrorAction = [System.Management.Automation.ActionPreference]::Stop
26 | }
27 |
28 | $Silent = @{
29 | ErrorAction = [System.Management.Automation.ActionPreference]::Ignore
30 | WarningAction = [System.Management.Automation.ActionPreference]::Ignore
31 | }
32 |
33 | $Manifest = Test-ModuleManifest -Path "$PSScriptRoot/module/$ModuleName.psd1" @Silent
34 | $Version = $Manifest.Version
35 | $PowerShellPath = "$PSScriptRoot/module"
36 | $CSharpPath = "$PSScriptRoot/src"
37 | $ReleasePath = "$PSScriptRoot/Release/$ModuleName/$Version"
38 | $DocsPath = "$PSScriptRoot/docs"
39 | $TestPath = "$PSScriptRoot/test/$ModuleName.Tests"
40 | $ToolsPath = "$PSScriptRoot/tools"
41 | $HasDocs = Test-Path $DocsPath/$Culture/*.md
42 | $HasTests = Test-Path $TestPath/*
43 | $IsUnix = $PSEdition -eq 'Core' -and -not $IsWindows
44 |
45 | function InvokeWithPSModulePath([scriptblock] $Action) {
46 | $oldPSModulePath = $env:PSModulePath
47 | try {
48 | $env:PSModulePath = Join-Path (Split-Path $pwsh.Path) -ChildPath 'Modules'
49 | . $Action
50 | } finally {
51 | $env:PSModulePath = $oldPSModulePath
52 | }
53 | }
54 |
55 | task Clean {
56 | if (Test-Path $ReleasePath) {
57 | Remove-Item $ReleasePath -Recurse
58 | }
59 |
60 | New-Item -ItemType Directory $ReleasePath | Out-Null
61 | }
62 |
63 | task BuildDocs -If { $HasDocs } {
64 | New-ExternalHelp -Path $DocsPath/$Culture -OutputPath $ReleasePath/$Culture | Out-Null
65 | }
66 |
67 | task AssertDependencies AssertPowerShellCore, AssertRequiredModules, AssertDotNet, AssertOpenCover
68 |
69 | task AssertOpenCover -If { $GenerateCodeCoverage.IsPresent } {
70 | if ($Discovery.IsUnix) {
71 | Write-Warning 'Generating code coverage from .NET core is currently unsupported, disabling code coverage generation.'
72 | $script:GenerateCodeCoverage = $false
73 | return
74 | }
75 |
76 | $script:openCover = & "$ToolsPath\GetOpenCover.ps1" -Force:$Force.IsPresent
77 | }
78 |
79 | task AssertPowerShellCore {
80 | $script:pwsh = $pwsh = Get-Command pwsh @Silent
81 | if ($pwsh) {
82 | return
83 | }
84 |
85 | if ($Force.IsPresent) {
86 | choco install powershell-core --version 6.2.2 -y
87 | } else {
88 | choco install powershell-core --verison 6.2.2
89 | }
90 |
91 | $script:pwsh = Get-Command $env:ProgramFiles/PowerShell/6/pwsh.exe @FailOnError
92 | }
93 |
94 | task AssertRequiredModules {
95 | $assertRequiredModule = Get-Command $ToolsPath/AssertRequiredModule.ps1 @FailOnError
96 | & $assertRequiredModule platyPS -RequiredVersion 0.14.0 -Force:$Force.IsPresent
97 | & $assertRequiredModule Pester -RequiredVersion 4.8.1 -Force:$Force.IsPresent
98 | }
99 |
100 | task AssertDotNet {
101 | $script:dotnet = & $ToolsPath/GetDotNet.ps1 -Unix:$IsUnix
102 | }
103 |
104 | task AssertPSResGen {
105 | # Download the ResGen tool used by PowerShell core internally. This will need to be replaced
106 | # when the dotnet cli gains support for it.
107 | if (-not (Test-Path $ToolsPath/ResGen)) {
108 | New-Item -ItemType Directory $ToolsPath/ResGen | Out-Null
109 | }
110 |
111 | if (-not (Test-Path $ToolsPath/ResGen/Program.cs)) {
112 | $programUri = 'https://raw.githubusercontent.com/PowerShell/PowerShell/v6.2.2/src/ResGen/Program.cs'
113 | Invoke-WebRequest $programUri -OutFile $ToolsPath/ResGen/Program.cs @FailOnError
114 | }
115 |
116 | if (-not (Test-Path $ToolsPath/ResGen/ResGen.csproj)) {
117 | $projUri = 'https://raw.githubusercontent.com/PowerShell/PowerShell/v6.2.2/src/ResGen/ResGen.csproj'
118 | Invoke-WebRequest $projUri -OutFile $ToolsPath/ResGen/ResGen.csproj @FailOnError
119 | }
120 | }
121 |
122 | task ResGenImpl {
123 | Push-Location $CSharpPath/$ModuleName
124 | try {
125 | & $dotnet run --project $ToolsPath/ResGen/ResGen.csproj
126 | } finally {
127 | Pop-Location
128 | }
129 | }
130 |
131 | task BuildManaged {
132 | & $dotnet publish --framework $Framework --configuration $Configuration --verbosity q -nologo
133 | }
134 |
135 | task CopyToRelease {
136 | $splat = @{
137 | Destination = $ReleasePath
138 | Force = $true
139 | ErrorAction = [System.Management.Automation.ActionPreference]::Stop
140 | }
141 |
142 | $itemsToCopy = (
143 | "$ModuleName.psm1",
144 | "$ModuleName.psd1",
145 | "$ModuleName.format.ps1xml",
146 | "$ModuleName.types.ps1xml")
147 |
148 | foreach ($itemName in $itemsToCopy) {
149 | $splat['LiteralPath'] = Join-Path $PowerShellPath -ChildPath $itemName
150 | Copy-Item @splat
151 | }
152 |
153 | $itemsToCopy = (
154 | "$ModuleName.deps.json",
155 | "$ModuleName.dll",
156 | "$ModuleName.pdb",
157 | "$ModuleName.xml")
158 |
159 | $publishPath = Join-Path $CSharpPath -ChildPath "$ModuleName/bin/$Configuration/$Framework/publish"
160 | foreach ($itemName in $itemsToCopy) {
161 | $splat['LiteralPath'] = Join-Path $publishPath -ChildPath $itemName
162 | Copy-Item @splat
163 | }
164 | }
165 |
166 | task Analyze -If { $ShouldAnalyze } {
167 | Invoke-ScriptAnalyzer -Path $ReleasePath -Settings $PSScriptRoot/ScriptAnalyzerSettings.psd1 -Recurse
168 | }
169 |
170 | task DoTest {
171 | if ($Discovery.IsUnix) {
172 | $scriptString = '
173 | $projectPath = "{0}"
174 | Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml"
175 | ' -f $PSScriptRoot
176 | } else {
177 | $scriptString = '
178 | Set-ExecutionPolicy Bypass -Force -Scope Process
179 | $projectPath = "{0}"
180 | Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml"
181 | ' -f $PSScriptRoot
182 | }
183 |
184 | $encodedCommand =
185 | [convert]::ToBase64String(
186 | [System.Text.Encoding]::Unicode.GetBytes(
187 | $scriptString))
188 |
189 | $powershellCommand = 'powershell'
190 | if ($Discovery.IsUnix) {
191 | $powershellCommand = 'pwsh'
192 | }
193 |
194 | $powershell = (Get-Command $powershellCommand).Source
195 |
196 | if (-not $GenerateCodeCoverage.IsPresent) {
197 | & $powershell -NoProfile -EncodedCommand $encodedCommand
198 | return
199 | }
200 |
201 | # OpenCover needs full pdb's. I'm very open to suggestions for streamlining this...
202 | & $dotnet clean --verbosity q -nologo
203 | & $dotnet build --configuration $Configuration --framework netstandard2.0 --verbosity q -nologo /p:DebugType=Full
204 |
205 | $release = Join-Path $ReleasePath -ChildPath $ModuleName
206 | $coverage = Join-Path $CSharpPath -ChildPath "$ModuleName/bin/$Configuration/$Framework/$ModuleName"
207 |
208 | Rename-Item "$release.pdb" -NewName "$ModuleName.pdb.tmp"
209 | Rename-Item "$release.dll" -NewName "$ModuleName.dll.tmp"
210 | Copy-Item "$coverage.pdb" "$release.pdb"
211 | Copy-Item "$coverage.dll" "$release.dll"
212 |
213 | & $openCover `
214 | -target:$powershell `
215 | -register:user `
216 | -output:$PSScriptRoot\testresults\opencover.xml `
217 | -hideskipped:all `
218 | -filter:+[PSLambda*]* `
219 | -targetargs:"-NoProfile -EncodedCommand $encodedCommand"
220 |
221 | Remove-Item "$release.pdb"
222 | Remove-Item "$release.dll"
223 | Rename-Item "$release.pdb.tmp" -NewName "$ModuleName.pdb"
224 | Rename-Item "$release.dll.tmp" -NewName "$ModuleName.dll"
225 | }
226 |
227 | task DoInstall {
228 | $installBase = $Home
229 | if ($profile) {
230 | $installBase = $profile | Split-Path
231 | }
232 |
233 | $installPath = "$installBase/Modules/$ModuleName/$Version"
234 | if (-not (Test-Path $installPath)) {
235 | $null = New-Item $installPath -ItemType Directory
236 | }
237 |
238 | Copy-Item -Path $ReleasePath/* -Destination $installPath -Force -Recurse
239 | }
240 |
241 | task DoPublish {
242 | if (-not (Test-Path $env:USERPROFILE/.PSGallery/apikey.xml)) {
243 | throw 'Could not find PSGallery API key!'
244 | }
245 |
246 | $apiKey = (Import-Clixml $env:USERPROFILE/.PSGallery/apikey.xml).GetNetworkCredential().Password
247 | Publish-Module -Name $ReleasePath -NuGetApiKey $apiKey -Confirm
248 | }
249 |
250 | task ResGen -Jobs AssertPSResGen, ResGenImpl
251 |
252 | task Build -Jobs Clean, AssertDependencies, ResGen, BuildManaged, CopyToRelease, BuildDocs
253 |
254 | task Test -Jobs Build, DoTest
255 |
256 | task PrePublish -Jobs Test
257 |
258 | task Install -Jobs Test, DoInstall
259 |
260 | task Publish -Jobs Test, DoPublish
261 |
262 | task . Build
263 |
--------------------------------------------------------------------------------
/src/PSLambda/VariableScope.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 |
6 | namespace PSLambda
7 | {
8 | ///
9 | /// Represents the variable scope for a single block .
10 | ///
11 | internal class VariableScope
12 | {
13 | private static readonly string[] s_dollarUnderNameCache =
14 | {
15 | "PSItem", "PSItem2", "PSItem3", "PSItem4", "PSItem5",
16 | "PSItem6", "PSItem7", "PSItem8", "PSItem9", "PSItem10",
17 | };
18 |
19 | private (ParameterExpression Parameter, int Version) _dollarUnder;
20 |
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | /// Parameters to include in the scope.
25 | public VariableScope(ParameterExpression[] parameters)
26 | {
27 | Parameters = parameters.ToDictionary(p => p.Name);
28 | }
29 |
30 | ///
31 | /// Initializes a new instance of the class.
32 | ///
33 | /// The parent object.
34 | public VariableScope(VariableScope parent)
35 | {
36 | Parent = parent;
37 | }
38 |
39 | ///
40 | /// Gets the parent .
41 | ///
42 | internal VariableScope Parent { get; }
43 |
44 | ///
45 | /// Gets the parameter objects for the current scope.
46 | ///
47 | internal Dictionary Parameters { get; } = new Dictionary();
48 |
49 | ///
50 | /// Gets the variable objects for the current scope.
51 | ///
52 | internal Dictionary Variables { get; } = new Dictionary();
53 |
54 | ///
55 | /// Creates a new variable expression without looking at previous scopes.
56 | ///
57 | /// The name of the variable.
58 | /// The type of the variable.
59 | /// The variable .
60 | internal ParameterExpression NewVariable(string name, Type type)
61 | {
62 | ParameterExpression variable = Expression.Variable(type, name);
63 | Variables.Add(name, variable);
64 | return variable;
65 | }
66 |
67 | ///
68 | /// Gets an already defined variable if it already exists,
69 | /// otherwise one is created.
70 | ///
71 | /// The name of the variable.
72 | /// The type of the variable.
73 | /// The variable .
74 | internal ParameterExpression GetOrCreateVariable(string name, Type type)
75 | {
76 | return GetOrCreateVariable(name, type, out _);
77 | }
78 |
79 | ///
80 | /// Gets an already defined variable if it already exists,
81 | /// otherwise one is created.
82 | ///
83 | /// The name of the variable.
84 | /// The type of the variable.
85 | ///
86 | /// A value indicating whether the variable has already been defined.
87 | ///
88 | /// The variable .
89 | internal ParameterExpression GetOrCreateVariable(string name, Type type, out bool alreadyDefined)
90 | {
91 | if (TryGetVariable(name, out ParameterExpression existingVar))
92 | {
93 | alreadyDefined = true;
94 | return existingVar;
95 | }
96 |
97 | alreadyDefined = false;
98 | var newVariable = Expression.Parameter(type ?? typeof(object), name);
99 | Variables.Add(name, newVariable);
100 | return newVariable;
101 | }
102 |
103 | ///
104 | /// Gets an already defined variable if it already exists,
105 | /// otherwise one is created.
106 | ///
107 | /// The name of the variable.
108 | ///
109 | /// A function that retrieves the variable's type if it has not already been defined.
110 | ///
111 | /// The variable .
112 | internal ParameterExpression GetOrCreateVariable(string name, Func typeGetter)
113 | {
114 | if (TryGetVariable(name, out ParameterExpression existingVar))
115 | {
116 | return existingVar;
117 | }
118 |
119 | var newVariable = Expression.Parameter(typeGetter() ?? typeof(object), name);
120 | Variables.Add(name, newVariable);
121 | return newVariable;
122 | }
123 |
124 | ///
125 | /// Attempts to get a variable that has already been defined.
126 | ///
127 | /// The name of the variable.
128 | /// The already defined variable .
129 | ///
130 | /// A value indicating whether an existing variable was found.
131 | ///
132 | internal bool TryGetVariable(string name, out ParameterExpression existingVariable)
133 | {
134 | if (Variables.TryGetValue(name, out existingVariable))
135 | {
136 | return true;
137 | }
138 |
139 | if (Parameters.TryGetValue(name, out existingVariable))
140 | {
141 | return true;
142 | }
143 |
144 | if (Parent != null && Parent.TryGetVariable(name, out existingVariable))
145 | {
146 | return true;
147 | }
148 |
149 | return false;
150 | }
151 |
152 | ///
153 | /// Gets the automatic variable $_ or $PSItem from the
154 | /// current scope, or closest parent scope where it is defined.
155 | ///
156 | ///
157 | /// The parameter expression referencing dollar under if defined;
158 | /// otherwise .
159 | ///
160 | internal ParameterExpression GetDollarUnder()
161 | {
162 | for (VariableScope current = this; current != null; current = current.Parent)
163 | {
164 | if (current._dollarUnder.Parameter != null)
165 | {
166 | return current._dollarUnder.Parameter;
167 | }
168 | }
169 |
170 | return null;
171 | }
172 |
173 | ///
174 | /// Sets the automatic variable $_ or $PSItem in the current
175 | /// scope. If the variable exists in a parent scope and the type is the same,
176 | /// then the parent variable will be used. If the variable exists in a parent
177 | /// scope but the type is not the same, the variable will be renamed behind
178 | /// the scenes.
179 | ///
180 | ///
181 | /// The static type that dollar under should contain.
182 | ///
183 | ///
184 | /// The parameter expression referencing dollar under.
185 | ///
186 | internal ParameterExpression SetDollarUnder(Type type)
187 | {
188 | static string GetDollarUnderName(int version)
189 | {
190 | if (version < s_dollarUnderNameCache.Length)
191 | {
192 | return s_dollarUnderNameCache[version];
193 | }
194 |
195 | return string.Concat("PSItem", version);
196 | }
197 |
198 | for (VariableScope scope = Parent; scope != null; scope = scope.Parent)
199 | {
200 | var dollarUnder = scope._dollarUnder;
201 | if (dollarUnder.Parameter == null)
202 | {
203 | continue;
204 | }
205 |
206 | if (_dollarUnder.Parameter.Type == type)
207 | {
208 | _dollarUnder = dollarUnder;
209 | return _dollarUnder.Parameter;
210 | }
211 |
212 | var version = dollarUnder.Version + 1;
213 | _dollarUnder = (Expression.Parameter(type, GetDollarUnderName(version)), version);
214 | return _dollarUnder.Parameter;
215 | }
216 |
217 | _dollarUnder = (Expression.Parameter(type, "PSItem"), 0);
218 | return _dollarUnder.Parameter;
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/PSLambda/ReflectionCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Management.Automation;
6 | using System.Reflection;
7 | using System.Text.RegularExpressions;
8 |
9 | namespace PSLambda
10 | {
11 | ///
12 | /// Provides cached objects for generating method and property invocation
13 | /// objects.
14 | ///
15 | internal static class ReflectionCache
16 | {
17 | ///
18 | /// Resolves to .
19 | ///
20 | public static readonly MethodInfo LanguagePrimitives_GetEnumerator =
21 | typeof(LanguagePrimitives).GetMethod("GetEnumerator", new[] { typeof(object) });
22 |
23 | ///
24 | /// Resolves to .
25 | ///
26 | public static readonly MethodInfo LanguagePrimitives_IsTrue =
27 | typeof(LanguagePrimitives).GetMethod("IsTrue", new[] { typeof(object) });
28 |
29 | ///
30 | /// Resolves to .
31 | ///
32 | public static readonly MethodInfo LanguagePrimitives_Compare =
33 | typeof(LanguagePrimitives).GetMethod("Compare", new[] { typeof(object), typeof(object), typeof(bool) });
34 |
35 | ///
36 | /// Resolves to .
37 | ///
38 | public static readonly MethodInfo LanguagePrimitives_ConvertToGeneric =
39 | typeof(LanguagePrimitives).GetMethod("ConvertTo", new[] { typeof(object) });
40 |
41 | ///
42 | /// Resolves to .
43 | ///
44 | public static readonly MethodInfo LanguagePrimitives_ConvertTo =
45 | typeof(LanguagePrimitives).GetMethod("ConvertTo", new[] { typeof(object), typeof(Type) });
46 |
47 | ///
48 | /// Resolves to .
49 | ///
50 | public static readonly MethodInfo LangaugePrimitives_TryConvertToGeneric =
51 | (MethodInfo)typeof(LanguagePrimitives).FindMembers(
52 | MemberTypes.Method,
53 | BindingFlags.Static | BindingFlags.Public,
54 | (m, filterCriteria) =>
55 | {
56 | var method = m as MethodInfo;
57 | if (!method.IsGenericMethod)
58 | {
59 | return false;
60 | }
61 |
62 | var parameters = method.GetParameters();
63 | return parameters.Length == 2
64 | && parameters[0].ParameterType == typeof(object)
65 | && parameters[1].ParameterType.IsByRef;
66 | },
67 | null)
68 | .FirstOrDefault();
69 |
70 | ///
71 | /// Resolves to .
72 | ///
73 | public static readonly ConstructorInfo WildcardPattern_Ctor =
74 | typeof(WildcardPattern).GetConstructor(new[] { typeof(string), typeof(WildcardOptions) });
75 |
76 | ///
77 | /// Resolves to .
78 | ///
79 | public static readonly MethodInfo Regex_IsMatch =
80 | typeof(Regex).GetMethod("IsMatch", new[] { typeof(string), typeof(string), typeof(RegexOptions) });
81 |
82 | ///
83 | /// Resolves to .
84 | ///
85 | public static readonly MethodInfo Regex_Replace =
86 | typeof(Regex).GetMethod("Replace", new[] { typeof(string), typeof(string), typeof(string), typeof(RegexOptions) });
87 |
88 | ///
89 | /// Resolves to .
90 | ///
91 | public static readonly MethodInfo Regex_Split =
92 | typeof(Regex).GetMethod("Split", new[] { typeof(string), typeof(string), typeof(RegexOptions) });
93 |
94 | ///
95 | /// Resolves to .
96 | ///
97 | public static readonly MethodInfo WildcardPattern_IsMatch =
98 | typeof(WildcardPattern).GetMethod("IsMatch", new[] { typeof(string) });
99 |
100 | ///
101 | /// Resolves to .
102 | ///
103 | public static readonly MethodInfo IEnumerator_MoveNext =
104 | typeof(IEnumerator).GetMethod("MoveNext");
105 |
106 | ///
107 | /// Resolves to .
108 | ///
109 | public static readonly MethodInfo String_Join =
110 | typeof(string).GetMethod("Join", new[] { typeof(string), typeof(string[]) });
111 |
112 | ///
113 | /// Resolves to .
114 | ///
115 | public static readonly MethodInfo String_Format =
116 | typeof(string).GetMethod("Format", new[] { typeof(IFormatProvider), typeof(string), typeof(object[]) });
117 |
118 | ///
119 | /// Resolves to .
120 | ///
121 | public static readonly PropertyInfo CultureInfo_CurrentCulture =
122 | typeof(System.Globalization.CultureInfo).GetProperty("CurrentCulture");
123 |
124 | ///
125 | /// Resolves to the non-public type System.Management.Automation.ArgumentTypeConverterAttribute.
126 | ///
127 | public static readonly Type ArgumentTypeConverterAttribute =
128 | typeof(PSObject).Assembly.GetType("System.Management.Automation.ArgumentTypeConverterAttribute");
129 |
130 | ///
131 | /// Resolves to the non-public property System.Management.Automation.ArgumentTypeConverterAttribute.TargetType.
132 | ///
133 | public static readonly PropertyInfo ArgumentTypeConverterAttribute_TargetType =
134 | ArgumentTypeConverterAttribute?.GetProperty("TargetType", BindingFlags.Instance | BindingFlags.NonPublic);
135 |
136 | ///
137 | /// Resolves to .
138 | ///
139 | public static readonly ConstructorInfo RuntimeException_Ctor =
140 | typeof(RuntimeException).GetConstructor(new[] { typeof(string) });
141 |
142 | ///
143 | /// Resolves to .
144 | ///
145 | public static readonly MethodInfo IDisposable_Dispose =
146 | typeof(IDisposable).GetMethod("Dispose");
147 |
148 | ///
149 | /// Resolves to the indexer for .
150 | ///
151 | public static readonly PropertyInfo IList_Item =
152 | typeof(IList).GetProperty("Item");
153 |
154 | ///
155 | /// Resolves to the indexer for .
156 | ///
157 | public static readonly PropertyInfo IDictionary_Item =
158 | typeof(IDictionary).GetProperty("Item");
159 |
160 | ///
161 | /// Resolves to the non-public parameterless constructor for System.Management.Automation.ExitException.
162 | ///
163 | public static readonly ConstructorInfo ExitException_Ctor =
164 | typeof(ExitException).GetConstructor(
165 | BindingFlags.Instance | BindingFlags.NonPublic,
166 | null,
167 | new Type[0],
168 | new ParameterModifier[0]);
169 |
170 | ///
171 | /// Resolves to the non-public constructor System.Management.Automation.ExitException(Object).
172 | ///
173 | public static readonly ConstructorInfo ExitException_Ctor_Object =
174 | typeof(ExitException).GetConstructor(
175 | BindingFlags.Instance | BindingFlags.NonPublic,
176 | null,
177 | new[] { typeof(object) },
178 | new[] { new ParameterModifier(1) });
179 |
180 | ///
181 | /// Resolves to .
182 | ///
183 | public static readonly MethodInfo Hashtable_Add =
184 | typeof(Hashtable).GetMethod("Add", new[] { typeof(object), typeof(object) });
185 |
186 | ///
187 | /// Resolves to .
188 | ///
189 | public static readonly ConstructorInfo Hashtable_Ctor =
190 | typeof(Hashtable).GetConstructor(new[] { typeof(int), typeof(IEqualityComparer) });
191 |
192 | ///
193 | /// Resolves to .
194 | ///
195 | public static readonly MethodInfo IEnumerator_get_Current =
196 | typeof(IEnumerator).GetMethod(Strings.EnumeratorGetCurrentMethodName, Type.EmptyTypes);
197 |
198 | ///
199 | /// Resolves to .
200 | ///
201 | public static readonly MethodInfo IEnumerable_GetEnumerator =
202 | typeof(IEnumerable).GetMethod(Strings.GetEnumeratorMethodName, Type.EmptyTypes);
203 |
204 | ///
205 | /// Resolves to .
206 | ///
207 | public static readonly PropertyInfo StringComparer_CurrentCultureIgnoreCase =
208 | typeof(StringComparer).GetProperty("CurrentCultureIgnoreCase");
209 |
210 | ///
211 | /// Resolves to .
212 | ///
213 | public static readonly MethodInfo ExpressionUtils_PSEqualsIgnoreCase =
214 | typeof(ExpressionUtils).GetMethod("PSEqualsIgnoreCase", BindingFlags.Static | BindingFlags.NonPublic);
215 |
216 | ///
217 | /// Resolves to .
218 | ///
219 | public static readonly MethodInfo ExpressionUtils_GetRange =
220 | typeof(ExpressionUtils).GetMethod("GetRange", BindingFlags.Static | BindingFlags.NonPublic);
221 |
222 | ///
223 | /// Resolves to .
224 | ///
225 | public static readonly MethodInfo Monitor_Enter =
226 | typeof(System.Threading.Monitor).GetMethod(
227 | "Enter",
228 | new[] { typeof(object), typeof(bool).MakeByRefType() });
229 |
230 | ///
231 | /// Resolves to .
232 | ///
233 | public static readonly MethodInfo Monitor_Exit =
234 | typeof(System.Threading.Monitor).GetMethod("Exit", new[] { typeof(object) });
235 |
236 | ///
237 | /// Resolves to .
238 | ///
239 | public static readonly MethodInfo IEnumerable_T_GetEnumerator =
240 | typeof(IEnumerable<>).GetMethod(Strings.GetEnumeratorMethodName, Type.EmptyTypes);
241 |
242 | ///
243 | /// Resolves to .
244 | ///
245 | public static readonly MethodInfo IEnumerator_T_get_Current =
246 | typeof(IEnumerator<>).GetMethod(Strings.EnumeratorGetCurrentMethodName, Type.EmptyTypes);
247 |
248 | ///
249 | /// Resolves to .
250 | ///
251 | public static readonly MethodInfo IDictionary_GetEnumerator =
252 | typeof(IDictionary).GetMethod(Strings.GetEnumeratorMethodName, Type.EmptyTypes);
253 |
254 | ///
255 | /// Resolves to .
256 | ///
257 | public static readonly MethodInfo IDictionaryEnumerator_get_Entry =
258 | typeof(IDictionaryEnumerator).GetMethod("get_Entry", Type.EmptyTypes);
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/src/PSLambda/DelegateSyntaxVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Management.Automation.Language;
4 |
5 | namespace PSLambda
6 | {
7 | ///
8 | /// Provides the ability to reshape a when it fits
9 | /// the custom syntax for anonymous method expressions.
10 | ///
11 | internal class DelegateSyntaxVisitor : ICustomAstVisitor
12 | {
13 | private readonly IParseErrorWriter _errorWriter;
14 |
15 | private readonly List> _variables = new List>();
16 |
17 | private IScriptExtent _paramBlockExtent;
18 |
19 | private bool _failed;
20 |
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | /// The to report errors to.
25 | internal DelegateSyntaxVisitor(IParseErrorWriter errorWriter)
26 | {
27 | _errorWriter = errorWriter;
28 | }
29 |
30 | #pragma warning disable SA1600
31 | public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)
32 | {
33 | return scriptBlockExpressionAst.ScriptBlock.Visit(this);
34 | }
35 |
36 | public object VisitScriptBlock(ScriptBlockAst scriptBlockAst)
37 | {
38 | if (scriptBlockAst.ParamBlock != null)
39 | {
40 | return scriptBlockAst;
41 | }
42 |
43 | if (scriptBlockAst.BeginBlock != null)
44 | {
45 | _errorWriter.ReportNotSupported(
46 | scriptBlockAst.BeginBlock.Extent,
47 | nameof(CompilerStrings.BeginBlock),
48 | CompilerStrings.BeginBlock);
49 | return null;
50 | }
51 |
52 | if (scriptBlockAst.ProcessBlock != null)
53 | {
54 | _errorWriter.ReportNotSupported(
55 | scriptBlockAst.ProcessBlock.Extent,
56 | nameof(CompilerStrings.ProcessBlock),
57 | CompilerStrings.ProcessBlock);
58 | return null;
59 | }
60 |
61 | if (scriptBlockAst.EndBlock == null)
62 | {
63 | _errorWriter.ReportMissing(
64 | scriptBlockAst.Extent,
65 | nameof(CompilerStrings.EndBlock),
66 | CompilerStrings.EndBlock);
67 | return null;
68 | }
69 |
70 | var body = scriptBlockAst.EndBlock.Visit(this);
71 | if (_failed)
72 | {
73 | return scriptBlockAst;
74 | }
75 |
76 | _errorWriter.ThrowIfAnyErrors();
77 | var parameters = new ParameterAst[_variables.Count];
78 | for (var i = 0; i < parameters.Length; i++)
79 | {
80 | parameters[i] = new ParameterAst(
81 | _variables[i].Item2.Extent,
82 | (VariableExpressionAst)_variables[i].Item2.Copy(),
83 | new[] { new TypeConstraintAst(_variables[i].Item1.Extent, _variables[i].Item1) },
84 | null);
85 | }
86 |
87 | var paramBlock =
88 | new ParamBlockAst(
89 | _paramBlockExtent,
90 | Array.Empty(),
91 | parameters);
92 |
93 | return new ScriptBlockAst(
94 | scriptBlockAst.Extent,
95 | paramBlock,
96 | null,
97 | null,
98 | (NamedBlockAst)((Ast)body).Copy(),
99 | null);
100 | }
101 |
102 | public object VisitNamedBlock(NamedBlockAst namedBlockAst)
103 | {
104 | if (namedBlockAst.Statements == null ||
105 | namedBlockAst.Statements.Count == 0)
106 | {
107 | _errorWriter.ReportMissing(
108 | namedBlockAst.Extent,
109 | nameof(CompilerStrings.EndBlock),
110 | CompilerStrings.EndBlock);
111 | return null;
112 | }
113 |
114 | if (namedBlockAst.Statements.Count > 1)
115 | {
116 | _failed = true;
117 | return null;
118 | }
119 |
120 | var body = namedBlockAst.Statements[0].Visit(this);
121 | if (body == null)
122 | {
123 | _failed = true;
124 | return null;
125 | }
126 |
127 | return body;
128 | }
129 |
130 | public object VisitPipeline(PipelineAst pipelineAst)
131 | {
132 | return pipelineAst.PipelineElements[0].Visit(this);
133 | }
134 |
135 | public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst)
136 | {
137 | _paramBlockExtent = assignmentStatementAst.Left.Extent;
138 | DelegateParameterVisitor.AddVariables(
139 | assignmentStatementAst.Left,
140 | _variables);
141 |
142 | if (!(assignmentStatementAst.Right is PipelineAst pipeline))
143 | {
144 | return null;
145 | }
146 |
147 | if (!(pipeline.PipelineElements[0] is CommandAst commandAst) ||
148 | commandAst.GetCommandName() != Strings.DelegateSyntaxCommandName ||
149 | commandAst.CommandElements.Count != 2)
150 | {
151 | return null;
152 | }
153 |
154 | if (commandAst.CommandElements[1] is ScriptBlockExpressionAst sbAst)
155 | {
156 | return sbAst.ScriptBlock.EndBlock;
157 | }
158 |
159 | var expression = commandAst.CommandElements[1] as ExpressionAst;
160 |
161 | var statements =
162 | new StatementAst[]
163 | {
164 | new CommandExpressionAst(
165 | expression.Extent,
166 | (ExpressionAst)expression.Copy(),
167 | Array.Empty()),
168 | };
169 |
170 | var statementBlockAst = new StatementBlockAst(
171 | commandAst.CommandElements[1].Extent,
172 | statements,
173 | Array.Empty());
174 |
175 | return new NamedBlockAst(
176 | commandAst.CommandElements[1].Extent,
177 | TokenKind.End,
178 | statementBlockAst,
179 | unnamed: true);
180 | }
181 | #pragma warning restore SA1600
182 |
183 | private class DelegateParameterVisitor : AstVisitor
184 | {
185 | private static readonly ITypeName s_objectTypeName = new TypeName(
186 | Empty.Extent,
187 | typeof(object).FullName);
188 |
189 | private List> _variables = new List>();
190 |
191 | private DelegateParameterVisitor()
192 | {
193 | }
194 |
195 | public static void AddVariables(
196 | ExpressionAst expression,
197 | List> variables)
198 | {
199 | var visitor = new DelegateParameterVisitor
200 | {
201 | _variables = variables,
202 | };
203 |
204 | expression.Visit(visitor);
205 | }
206 |
207 | public override AstVisitAction VisitConvertExpression(ConvertExpressionAst convertExpressionAst)
208 | {
209 | if (convertExpressionAst.Child is VariableExpressionAst variable)
210 | {
211 | _variables.Add(
212 | Tuple.Create(
213 | convertExpressionAst.Attribute.TypeName,
214 | variable));
215 |
216 | return AstVisitAction.SkipChildren;
217 | }
218 |
219 | return AstVisitAction.StopVisit;
220 | }
221 |
222 | public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst)
223 | {
224 | _variables.Add(
225 | Tuple.Create(
226 | s_objectTypeName,
227 | variableExpressionAst));
228 |
229 | return AstVisitAction.SkipChildren;
230 | }
231 | }
232 |
233 | #pragma warning disable SA1600, SA1201, SA1516
234 | public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => null;
235 | public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => null;
236 | public object VisitAttribute(AttributeAst attributeAst) => null;
237 | public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => null;
238 | public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => null;
239 | public object VisitBlockStatement(BlockStatementAst blockStatementAst) => null;
240 | public object VisitBreakStatement(BreakStatementAst breakStatementAst) => null;
241 | public object VisitCatchClause(CatchClauseAst catchClauseAst) => null;
242 | public object VisitCommand(CommandAst commandAst) => null;
243 | public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) => null;
244 | public object VisitCommandParameter(CommandParameterAst commandParameterAst) => null;
245 | public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null;
246 | public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => null;
247 | public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => null;
248 | public object VisitDataStatement(DataStatementAst dataStatementAst) => null;
249 | public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => null;
250 | public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => null;
251 | public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => null;
252 | public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => null;
253 | public object VisitExitStatement(ExitStatementAst exitStatementAst) => null;
254 | public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) => null;
255 | public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => null;
256 | public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) => null;
257 | public object VisitForStatement(ForStatementAst forStatementAst) => null;
258 | public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) => null;
259 | public object VisitHashtable(HashtableAst hashtableAst) => null;
260 | public object VisitIfStatement(IfStatementAst ifStmtAst) => null;
261 | public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => null;
262 | public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => null;
263 | public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => null;
264 | public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => null;
265 | public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => null;
266 | public object VisitParamBlock(ParamBlockAst paramBlockAst) => null;
267 | public object VisitParameter(ParameterAst parameterAst) => null;
268 | public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => null;
269 | public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => null;
270 | public object VisitStatementBlock(StatementBlockAst statementBlockAst) => null;
271 | public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null;
272 | public object VisitSubExpression(SubExpressionAst subExpressionAst) => null;
273 | public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => null;
274 | public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => null;
275 | public object VisitTrap(TrapStatementAst trapStatementAst) => null;
276 | public object VisitTryStatement(TryStatementAst tryStatementAst) => null;
277 | public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => null;
278 | public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => null;
279 | public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => null;
280 | public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => null;
281 | public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => null;
282 | public object VisitWhileStatement(WhileStatementAst whileStatementAst) => null;
283 | #pragma warning restore SA1600, SA1201, SA1516
284 | }
285 | }
286 |
--------------------------------------------------------------------------------