diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/Modules/ModuleData.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/Modules/ModuleData.cs index 2953021e6..6e82c8111 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/Modules/ModuleData.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/Modules/ModuleData.cs @@ -13,9 +13,13 @@ namespace Microsoft.PowerShell.CrossCompatibility.Query /// public class ModuleData { + private readonly RuntimeData _parent; + private readonly Data.ModuleData _moduleData; - private readonly Lazy, IReadOnlyDictionary, IReadOnlyDictionary>> _commands; + private readonly Lazy, IReadOnlyDictionary>> _lazyCommands; + + private readonly Lazy>> _lazyAliases; /// /// Create a query object around a module data object. @@ -23,14 +27,16 @@ public class ModuleData /// The name of the module. /// The version of the module. /// The module data object. - public ModuleData(string name, Version version, Data.ModuleData moduleData) + public ModuleData(string name, Version version, RuntimeData parent, Data.ModuleData moduleData) { _moduleData = moduleData; + _parent = parent; Name = name; Version = version; - _commands = new Lazy, IReadOnlyDictionary, IReadOnlyDictionary>>(() => CreateCommandTables(moduleData.Functions, moduleData.Cmdlets, moduleData.Aliases)); + _lazyCommands = new Lazy, IReadOnlyDictionary>>(() => CreateCommandTables(moduleData.Functions, moduleData.Cmdlets)); + _lazyAliases = new Lazy>>(() => CreateAliasTable(_moduleData.Aliases)); } /// @@ -51,12 +57,12 @@ public ModuleData(string name, Version version, Data.ModuleData moduleData) /// /// Functions exported by the module. /// - public IReadOnlyDictionary Functions => _commands.Value.Item1; + public IReadOnlyDictionary Functions => _lazyCommands.Value.Item1; /// /// Cmdlets exported by the module. /// - public IReadOnlyDictionary Cmdlets => _commands.Value.Item2; + public IReadOnlyDictionary Cmdlets => _lazyCommands.Value.Item2; /// /// Variables exported by the module. @@ -66,16 +72,33 @@ public ModuleData(string name, Version version, Data.ModuleData moduleData) /// /// Aliases exported by the module. /// - public IReadOnlyDictionary Aliases => _commands.Value.Item3; + public IReadOnlyDictionary> Aliases => _lazyAliases.Value; + + private IReadOnlyDictionary> CreateAliasTable(IReadOnlyDictionary aliases) + { + if (aliases == null || aliases.Count == 0) + { + return null; + } + + var aliasTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair alias in aliases) + { + if (_parent.Aliases.TryGetValue(alias.Key, out IReadOnlyList aliasedCommands)) + { + aliasTable[alias.Key] = aliasedCommands; + } + } - private static Tuple, IReadOnlyDictionary, IReadOnlyDictionary> CreateCommandTables( + return aliasTable; + } + + private static Tuple, IReadOnlyDictionary> CreateCommandTables( IReadOnlyDictionary functions, - IReadOnlyDictionary cmdlets, - IReadOnlyDictionary aliases) + IReadOnlyDictionary cmdlets) { Dictionary funcDict = null; Dictionary cmdletDict = null; - Dictionary aliasDict = null; if (functions != null) { @@ -95,29 +118,9 @@ private static Tuple, IReadOnlyDiction } } - if (aliases != null) - { - aliasDict = new Dictionary(aliases.Count, StringComparer.OrdinalIgnoreCase); - foreach (KeyValuePair alias in aliases) - { - if (funcDict != null && funcDict.TryGetValue(alias.Value, out FunctionData function)) - { - aliasDict[alias.Key] = function; - continue; - } - - if (cmdletDict != null && cmdletDict.TryGetValue(alias.Value, out CmdletData cmdlet)) - { - aliasDict[alias.Key] = cmdlet; - continue; - } - } - } - - return new Tuple, IReadOnlyDictionary, IReadOnlyDictionary>( + return new Tuple, IReadOnlyDictionary>( funcDict, - cmdletDict, - aliasDict); + cmdletDict); } } } diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs index 622a3a1f4..98db0616c 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using Data = Microsoft.PowerShell.CrossCompatibility.Data; namespace Microsoft.PowerShell.CrossCompatibility.Query @@ -14,6 +16,10 @@ public class RuntimeData { private readonly Lazy>> _modules; + private readonly Lazy>> _nonAliasCommands; + + private readonly Lazy>> _aliases; + private readonly Lazy>> _commands; private readonly Lazy _nativeCommands; @@ -28,7 +34,9 @@ public RuntimeData(Data.RuntimeData runtimeData) Common = new CommonPowerShellData(runtimeData.Common); _modules = new Lazy>>(() => CreateModuleTable(runtimeData.Modules)); - _commands = new Lazy>>(() => CreateCommandLookupTable(Modules)); + _nonAliasCommands = new Lazy>>(() => CreateNonAliasCommandLookupTable(Modules)); + _aliases = new Lazy>>(() => CreateAliasLookupTable(runtimeData.Modules, NonAliasCommands)); + _commands = new Lazy>>(() => new DualLookupTable>(NonAliasCommands, Aliases)); _nativeCommands = new Lazy(() => NativeCommandLookupTable.Create(runtimeData.NativeCommands)); } @@ -58,7 +66,11 @@ public RuntimeData(Data.RuntimeData runtimeData) /// public CommonPowerShellData Common { get; } - private static IReadOnlyDictionary> CreateModuleTable(IDictionary> modules) + internal IReadOnlyDictionary> NonAliasCommands => _nonAliasCommands.Value; + + internal IReadOnlyDictionary> Aliases => _aliases.Value; + + private IReadOnlyDictionary> CreateModuleTable(IDictionary> modules) { var moduleDict = new Dictionary>(modules.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair> moduleVersions in modules) @@ -66,14 +78,14 @@ private static IReadOnlyDictionary(moduleVersions.Value.Count); foreach (KeyValuePair module in moduleVersions.Value) { - moduleVersionDict[module.Key] = new ModuleData(name: moduleVersions.Key, version: module.Key, moduleData: module.Value); + moduleVersionDict[module.Key] = new ModuleData(name: moduleVersions.Key, version: module.Key, parent: this, moduleData: module.Value); } moduleDict[moduleVersions.Key] = moduleVersionDict; } return moduleDict; } - private static IReadOnlyDictionary> CreateCommandLookupTable( + private static IReadOnlyDictionary> CreateNonAliasCommandLookupTable( IReadOnlyDictionary> modules) { var commandTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -106,23 +118,94 @@ private static IReadOnlyDictionary> CreateCom ((List)commandTable[function.Key]).Add(function.Value); } } + } + } - if (module.Aliases != null) + return commandTable; + } + + private static IReadOnlyDictionary> CreateAliasLookupTable( + IReadOnlyDictionary> modules, + IReadOnlyDictionary> commands) + { + var aliasTable = new Dictionary>(); + foreach (KeyValuePair> module in modules) + { + foreach (KeyValuePair moduleVersion in module.Value) + { + if (moduleVersion.Value.Aliases == null) { - foreach (KeyValuePair alias in module.Aliases) - { - if (!commandTable.ContainsKey(alias.Key)) - { - commandTable.Add(alias.Key, new List()); - } + continue; + } - ((List)commandTable[alias.Key]).Add(alias.Value); + foreach (KeyValuePair alias in moduleVersion.Value.Aliases) + { + if (commands.TryGetValue(alias.Value, out IReadOnlyList aliasedCommands)) + { + aliasTable[alias.Key] = aliasedCommands; } } } } + return aliasTable; + } - return commandTable; + private class DualLookupTable : IReadOnlyDictionary + { + private readonly IReadOnlyDictionary _firstTable; + + private readonly IReadOnlyDictionary _secondTable; + + public DualLookupTable(IReadOnlyDictionary firstTable, IReadOnlyDictionary secondTable) + { + _firstTable = firstTable; + _secondTable = secondTable; + } + + public V this[K key] + { + get + { + if (_firstTable.TryGetValue(key, out V firstValue)) + { + return firstValue; + } + + if (_secondTable.TryGetValue(key, out V secondValue)) + { + return secondValue; + } + + throw new KeyNotFoundException(); + } + } + + public IEnumerable Keys => _firstTable.Keys.Concat(_secondTable.Keys); + + public IEnumerable Values => _firstTable.Values.Concat(_secondTable.Values); + + public int Count => _firstTable.Count + _secondTable.Count; + + public bool ContainsKey(K key) + { + return _firstTable.ContainsKey(key) || _secondTable.ContainsKey(key); + } + + public IEnumerator> GetEnumerator() + { + return _firstTable.Concat(_secondTable).GetEnumerator(); + } + + public bool TryGetValue(K key, out V value) + { + return _firstTable.TryGetValue(key, out value) + || _secondTable.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } } } diff --git a/Tests/Rules/UseCompatibleCommands.Tests.ps1 b/Tests/Rules/UseCompatibleCommands.Tests.ps1 index f94b32145..a3dbb2999 100644 --- a/Tests/Rules/UseCompatibleCommands.Tests.ps1 +++ b/Tests/Rules/UseCompatibleCommands.Tests.ps1 @@ -36,6 +36,8 @@ $script:CompatibilityTestCases = @( @{ Target = $script:Srv2012_3_profile; Script = 'Get-FileHash $pshome\powershell.exe | Format-List'; Commands = @("Get-FileHash"); Version = "3.0"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2012_3_profile; Script = 'Get-ChildItem ./ | Format-List'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } @{ Target = $script:Srv2012_3_profile; Script = 'Save-Help -Module $m -DestinationPath "C:\SavedHelp"'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2012_3_profile; Script = 'gci .'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2012_3_profile; Script = 'iex $expr | % { Transform $_ }'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } @{ Target = $script:Srv2012r2_4_profile; Script = 'Write-Information "Information"'; Commands = @("Write-Information"); Version = "4.0"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2012r2_4_profile; Script = '"Hello World" | ConvertFrom-String | Get-Member'; Commands = @("ConvertFrom-String"); Version = "4.0"; OS = "Windows"; ProblemCount = 1 } @@ -51,11 +53,16 @@ $script:CompatibilityTestCases = @( @{ Target = $script:Srv2012r2_4_profile; Script = 'Start-Job { Write-Host "Hello" } | Debug-Job'; Commands = @("Debug-Job"); Version = "4.0"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2012r2_4_profile; Script = 'Get-ItemPropertyValue -Path HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine -Name ApplicationBase'; Commands = @("Get-ItemPropertyValue"); Version = "4.0"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2012r2_4_profile; Script = 'Get-ChildItem ./ | Format-List'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2012r2_4_profile; Script = 'gci .'; Commands = @(); Version = "4.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2012r2_4_profile; Script = 'iex $expr | % { Transform $_ }'; Commands = @(); Version = "4.0"; OS = "Windows"; ProblemCount = 0 } @{ Target = $script:Srv2019_5_profile; Script = "Remove-Alias gcm"; Commands = @("Remove-Alias"); Version = "5.1"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2019_5_profile; Script = "Get-Uptime"; Commands = @("Get-Uptime"); Version = "5.1"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2019_5_profile; Script = "Remove-Service 'MyService'"; Commands = @("Remove-Service"); Version = "5.1"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2019_5_profile; Script = 'Get-ChildItem ./ | Format-List'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2019_5_profile; Script = 'gci .'; Commands = @(); Version = "5.1"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2019_5_profile; Script = 'iex $expr | % { Transform $_ }'; Commands = @(); Version = "5.1"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2019_5_profile; Script = 'fhx $filePath'; Commands = @(); Version = "5.1"; OS = "Windows"; ProblemCount = 0 } @{ Target = $script:Srv2019_6_1_profile; Script = "Add-PSSnapIn MySnapIn"; Commands = @("Add-PSSnapIn"); Version = "6.1"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2019_6_1_profile; Script = 'ConvertFrom-String $str'; Commands = @("ConvertFrom-String"); Version = "6.1"; OS = "Windows"; ProblemCount = 1 } @@ -88,13 +95,17 @@ $script:CompatibilityTestCases = @( @{ Target = $script:Srv2019_6_1_profile; Script = '$zip = New-WebServiceProxy -Uri "http://www.webservicex.net/uszip.asmx?WSDL"'; Commands = @("New-WebServiceProxy"); Version = "6.1"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2019_6_1_profile; Script = 'curl $uri'; Commands = @("curl"); Version = "6.1"; OS = "Windows"; ProblemCount = 1 } @{ Target = $script:Srv2019_6_1_profile; Script = 'Get-ChildItem ./ | Format-List'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2019_6_1_profile; Script = 'gci .'; Commands = @(); Version = "6.1"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Srv2016_6_1_profile; Script = 'iex $expr | % { Transform $_ }'; Commands = @(); Version = "6.1"; OS = "Windows"; ProblemCount = 0 } @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Get-AuthenticodeSignature ./script.ps1'; Commands = @("Get-AuthenticodeSignature"); Version = "6.1"; OS = "Linux"; ProblemCount = 1 } @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Get-Service systemd'; Commands = @("Get-Service"); Version = "6.1"; OS = "Linux"; ProblemCount = 1 } @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Start-Service -Name "sshd"'; Commands = @("Start-Service"); Version = "6.1"; OS = "Linux"; ProblemCount = 1 } @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Get-PSSessionConfiguration -Name Full | Format-List -Property *'; Commands = @("Get-PSSessionConfiguration"); Version = "6.1"; OS = "Linux"; ProblemCount = 1 } @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Get-CimInstance Win32_StartupCommand'; Commands = @("Get-CimInstance"); Version = "6.1"; OS = "Linux"; ProblemCount = 1 } - @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Get-ChildItem ./ | Format-List'; Commands = @(); Version = "3.0"; OS = "Windows"; ProblemCount = 0 } + @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'Get-ChildItem ./ | Format-List'; Commands = @(); Version = "6.1"; OS = "Linux"; ProblemCount = 0 } + @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'gci .'; Commands = @(); Version = "6.1"; OS = "Linux"; ProblemCount = 0 } + @{ Target = $script:Ubuntu1804_6_1_profile; Script = 'iex $expr | % { Transform $_ }'; Commands = @(); Version = "6.1"; OS = "Linux"; ProblemCount = 0 } ) $script:ParameterCompatibilityTestCases = @(