Team Ai
Datasetpublic

MegaBites-AI/Windows-powershell

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes372downloads
ScriptAnalysis.cs581 linesDownload Raw Back to Modules
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections;5using System.Collections.Generic;6using System.Globalization;7using System.IO;8using System.Management.Automation.Language;9using System.Text;10using System.Text.RegularExpressions;11 12namespace System.Management.Automation13{14    /// <summary>15    /// Class describing a PowerShell module...16    /// </summary>17    internal class ScriptAnalysis18    {19        internal static ScriptAnalysis Analyze(string path, ExecutionContext context)20        {21            ModuleIntrinsics.Tracer.WriteLine("Analyzing path: {0}", path);22 23            try24            {25                if (Utils.PathIsUnc(path) && (context.CurrentCommandProcessor.CommandRuntime != null))26                {27                    ProgressRecord analysisProgress = new ProgressRecord(0,28                        Modules.ScriptAnalysisPreparing,29                        string.Format(CultureInfo.InvariantCulture, Modules.ScriptAnalysisModule, path));30                    analysisProgress.RecordType = ProgressRecordType.Processing;31 32                    // Write the progress using a static source ID so that all33                    // analysis messages get single-threaded in the progress pane (rather than nesting).34                    context.CurrentCommandProcessor.CommandRuntime.WriteProgress(typeof(ScriptAnalysis).FullName.GetHashCode(), analysisProgress);35                }36            }37            catch (InvalidOperationException)38            {39                // This may be called when we are not allowed to write progress,40                // So eat the invalid operation41            }42 43            string scriptContent = File.ReadAllText(path, Encoding.Default);44 45            ParseError[] errors;46            var moduleAst = (new Parser()).Parse(path, scriptContent, null, out errors, ParseMode.ModuleAnalysis);47 48            // Don't bother analyzing if there are syntax errors (we don't do semantic analysis which would49            // detect other errors that we also might choose to ignore, but it's slower.)50            if (errors.Length > 0)51                return null;52 53            ExportVisitor exportVisitor = new ExportVisitor(forCompletion: false);54            moduleAst.Visit(exportVisitor);55 56            var result = new ScriptAnalysis57            {58                DiscoveredClasses = exportVisitor.DiscoveredClasses,59                DiscoveredExports = exportVisitor.DiscoveredExports,60                DiscoveredAliases = new Dictionary<string, string>(),61                DiscoveredModules = exportVisitor.DiscoveredModules,62                DiscoveredCommandFilters = exportVisitor.DiscoveredCommandFilters,63                AddsSelfToPath = exportVisitor.AddsSelfToPath64            };65 66            if (result.DiscoveredCommandFilters.Count == 0)67            {68                result.DiscoveredCommandFilters.Add("*");69            }70            else71            {72                // Post-process aliases, as they are not exported by default73                List<WildcardPattern> patterns = new List<WildcardPattern>();74                foreach (string discoveredCommandFilter in result.DiscoveredCommandFilters)75                {76                    patterns.Add(WildcardPattern.Get(discoveredCommandFilter, WildcardOptions.IgnoreCase));77                }78 79                foreach (var pair in exportVisitor.DiscoveredAliases)80                {81                    string discoveredAlias = pair.Key;82                    if (SessionStateUtilities.MatchesAnyWildcardPattern(discoveredAlias, patterns, defaultValue: false))83                    {84                        result.DiscoveredAliases[discoveredAlias] = pair.Value;85                    }86                }87            }88 89            return result;90        }91 92        internal List<string> DiscoveredExports { get; set; }93 94        internal Dictionary<string, string> DiscoveredAliases { get; set; }95 96        internal List<RequiredModuleInfo> DiscoveredModules { get; set; }97 98        internal List<string> DiscoveredCommandFilters { get; set; }99 100        internal bool AddsSelfToPath { get; set; }101 102        internal List<TypeDefinitionAst> DiscoveredClasses { get; set; }103    }104 105    // Defines the visitor that analyzes a script to determine its exports106    // and dependencies.107    internal class ExportVisitor : AstVisitor2108    {109        internal ExportVisitor(bool forCompletion)110        {111            _forCompletion = forCompletion;112            DiscoveredExports = new List<string>();113            DiscoveredFunctions = new Dictionary<string, FunctionDefinitionAst>(StringComparer.OrdinalIgnoreCase);114            DiscoveredAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);115            DiscoveredModules = new List<RequiredModuleInfo>();116            DiscoveredCommandFilters = new List<string>();117            DiscoveredClasses = new List<TypeDefinitionAst>();118        }119 120        static ExportVisitor()121        {122            var nameParam = new ParameterInfo { name = "Name", position = 0 };123            var valueParam = new ParameterInfo { name = "Value", position = 1 };124            var aliasParameterInfo = new ParameterBindingInfo { parameterInfo = new[] { nameParam, valueParam } };125 126            var functionParam = new ParameterInfo { name = "Function", position = -1 };127            var cmdletParam = new ParameterInfo { name = "Cmdlet", position = -1 };128            var aliasParam = new ParameterInfo { name = "Alias", position = -1 };129            var ipmoParameterInfo = new ParameterBindingInfo { parameterInfo = new[] { nameParam, functionParam, cmdletParam, aliasParam } };130 131            functionParam = new ParameterInfo { name = "Function", position = 0 };132            var exportModuleMemberInfo = new ParameterBindingInfo { parameterInfo = new[] { functionParam, cmdletParam, aliasParam } };133 134            s_parameterBindingInfoTable = new Dictionary<string, ParameterBindingInfo>(StringComparer.OrdinalIgnoreCase)135            {136                {"New-Alias",                                      aliasParameterInfo},137                {@"Microsoft.PowerShell.Utility\New-Alias",        aliasParameterInfo},138                {"Set-Alias",                                      aliasParameterInfo},139                {@"Microsoft.PowerShell.Utility\Set-Alias",        aliasParameterInfo},140                {"nal",                                            aliasParameterInfo},141                {"sal",                                            aliasParameterInfo},142                {"Import-Module",                                  ipmoParameterInfo},143                {@"Microsoft.PowerShell.Core\Import-Module",       ipmoParameterInfo},144                {"ipmo",                                           ipmoParameterInfo},145                {"Export-ModuleMember",                            exportModuleMemberInfo},146                {@"Microsoft.PowerShell.Core\Export-ModuleMember", exportModuleMemberInfo}147            };148        }149 150        private readonly bool _forCompletion;151 152        internal List<string> DiscoveredExports { get; set; }153 154        internal List<RequiredModuleInfo> DiscoveredModules { get; set; }155 156        internal Dictionary<string, FunctionDefinitionAst> DiscoveredFunctions { get; set; }157 158        internal Dictionary<string, string> DiscoveredAliases { get; set; }159 160        internal List<string> DiscoveredCommandFilters { get; set; }161 162        internal bool AddsSelfToPath { get; set; }163 164        internal List<TypeDefinitionAst> DiscoveredClasses { get; set; }165 166        public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst)167        {168            DiscoveredClasses.Add(typeDefinitionAst);169            return _forCompletion ? AstVisitAction.Continue : AstVisitAction.SkipChildren;170        }171 172        // Capture simple function definitions173        public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)174        {175            // Nested functions are ignored for the purposes of exports, but are still176            // recorded for command/parameter completion.177 178            // function Foo-Bar { ... }179 180            var functionName = functionDefinitionAst.Name;181            DiscoveredFunctions[functionName] = functionDefinitionAst;182            ModuleIntrinsics.Tracer.WriteLine("Discovered function definition: {0}", functionName);183 184            // Check if they've defined any aliases185            // function Foo-Bar { [Alias("Alias1", "...")] param() ... }186 187            var functionBody = functionDefinitionAst.Body;188            if ((functionBody.ParamBlock != null) && (functionBody.ParamBlock.Attributes != null))189            {190                foreach (AttributeAst attribute in functionBody.ParamBlock.Attributes)191                {192                    if (attribute.TypeName.GetReflectionAttributeType() == typeof(AliasAttribute))193                    {194                        foreach (ExpressionAst aliasAst in attribute.PositionalArguments)195                        {196                            var aliasExpression = aliasAst as StringConstantExpressionAst;197                            if (aliasExpression != null)198                            {199                                string alias = aliasExpression.Value;200 201                                DiscoveredAliases[alias] = functionName;202                                ModuleIntrinsics.Tracer.WriteLine("Function defines alias: {0} = {1}", alias, functionName);203                            }204                        }205                    }206                }207            }208 209            if (_forCompletion)210            {211                if (Ast.GetAncestorAst<ScriptBlockAst>(functionDefinitionAst).Parent == null)212                {213                    DiscoveredExports.Add(functionName);214                }215 216                return AstVisitAction.Continue;217            }218 219            DiscoveredExports.Add(functionName);220            return AstVisitAction.SkipChildren;221        }222 223        // Capture modules that add themselves to the path (so they generally package their functionality224        // as loose PS1 files)225        public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst)226        {227            // $env:PATH += "";$psScriptRoot""228            if (string.Equals("$env:PATH", assignmentStatementAst.Left.ToString(), StringComparison.OrdinalIgnoreCase) &&229                Regex.IsMatch(assignmentStatementAst.Right.ToString(), "\\$psScriptRoot", RegexOptions.IgnoreCase))230            {231                ModuleIntrinsics.Tracer.WriteLine("Module adds itself to the path.");232                AddsSelfToPath = true;233            }234 235            return AstVisitAction.SkipChildren;236        }237 238        // We skip a bunch of random statements because we can't really be accurate detecting functions/classes etc. that239        // are conditionally defined.240        public override AstVisitAction VisitIfStatement(IfStatementAst ifStmtAst) { return AstVisitAction.SkipChildren; }241 242        public override AstVisitAction VisitDataStatement(DataStatementAst dataStatementAst) { return AstVisitAction.SkipChildren; }243 244        public override AstVisitAction VisitForEachStatement(ForEachStatementAst forEachStatementAst) { return AstVisitAction.SkipChildren; }245 246        public override AstVisitAction VisitForStatement(ForStatementAst forStatementAst) { return AstVisitAction.SkipChildren; }247 248        public override AstVisitAction VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) { return AstVisitAction.SkipChildren; }249 250        public override AstVisitAction VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) { return AstVisitAction.SkipChildren; }251 252        public override AstVisitAction VisitWhileStatement(WhileStatementAst whileStatementAst) { return AstVisitAction.SkipChildren; }253 254        public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressionAst methodCallAst) { return AstVisitAction.SkipChildren; }255 256        public override AstVisitAction VisitSwitchStatement(SwitchStatementAst switchStatementAst) { return AstVisitAction.SkipChildren; }257 258        public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) { return AstVisitAction.SkipChildren; }259 260        // Visit one the other variations:261        //  - Dotting scripts262        //  - Setting aliases263        //  - Importing modules264        //  - Exporting module members265        public override AstVisitAction VisitCommand(CommandAst commandAst)266        {267            string commandName = commandAst.GetCommandName();268            if (commandName is null)269            {270                // GetCommandName only works if the name is a string constant. GetSafeValueVistor can evaluate some safe dynamic expressions271                try272                {273                    commandName = GetSafeValueVisitor.GetSafeValue(commandAst.CommandElements[0], null, GetSafeValueVisitor.SafeValueContext.ModuleAnalysis) as string;274                }275                catch (ParseException)276                {277                    // The script is invalid so we can't use GetSafeValue to get the name either.278                }279            }280 281            // We couldn't get the name of the command. Either it's an anonymous scriptblock: & {"Some script"}282            // Or it's a dynamic expression we couldn't safely resolve.283            if (commandName == null)284                return AstVisitAction.SkipChildren;285 286            // They are trying to dot a script287            if (commandAst.InvocationOperator == TokenKind.Dot)288            {289                // . Foo-Bar4.ps1290                // . $psScriptRoot\Foo-Bar.ps1 -Bing Baz291                // . ""$psScriptRoot\Support Files\Foo-Bar2.ps1"" -Bing Baz292                // . '$psScriptRoot\Support Files\Foo-Bar3.ps1' -Bing Baz293 294                DiscoveredModules.Add(295                    new RequiredModuleInfo { Name = commandName, CommandsToPostFilter = new List<string>() });296                ModuleIntrinsics.Tracer.WriteLine("Module dots {0}", commandName);297            }298 299            // They are setting an alias.300            if (string.Equals(commandName, "New-Alias", StringComparison.OrdinalIgnoreCase) ||301                string.Equals(commandName, "Microsoft.PowerShell.Utility\\New-Alias", StringComparison.OrdinalIgnoreCase) ||302                string.Equals(commandName, "Set-Alias", StringComparison.OrdinalIgnoreCase) ||303                string.Equals(commandName, "Microsoft.PowerShell.Utility\\Set-Alias", StringComparison.OrdinalIgnoreCase) ||304                string.Equals(commandName, "nal", StringComparison.OrdinalIgnoreCase) ||305                string.Equals(commandName, "sal", StringComparison.OrdinalIgnoreCase))306            {307                // Set-Alias Foo-Bar5 Foo-Bar308                // Set-Alias -Name Foo-Bar6 -Value Foo-Bar309                // sal Foo-Bar7 Foo-Bar310                // sal -Value Foo-Bar -Name Foo-Bar8311 312                var boundParameters = DoPsuedoParameterBinding(commandAst, commandName);313 314                var name = boundParameters["Name"] as string;315                if (!string.IsNullOrEmpty(name))316                {317                    var value = boundParameters["Value"] as string;318                    if (!string.IsNullOrEmpty(value))319                    {320                        // These aren't stored in DiscoveredExports, as they are only321                        // exported after the user calls Export-ModuleMember.322                        DiscoveredAliases[name] = value;323                        ModuleIntrinsics.Tracer.WriteLine("Module defines alias: {0} = {1}", name, value);324                    }325                }326 327                return AstVisitAction.SkipChildren;328            }329 330            // They are importing a module331            if (string.Equals(commandName, "Import-Module", StringComparison.OrdinalIgnoreCase) ||332                string.Equals(commandName, "ipmo", StringComparison.OrdinalIgnoreCase))333            {334                // Import-Module Module1335                // Import-Module Module2 -Function Foo-Module2*, Foo-Module2Second* -Cmdlet Foo-Module2Cmdlet,Foo-Module2Cmdlet*336                // Import-Module Module3 -Function Foo-Module3Command1, Foo-Module3Command2337                // Import-Module Module4,338                //    Module5339                // Import-Module -Name Module6,340                //    Module7 -Global341 342                var boundParameters = DoPsuedoParameterBinding(commandAst, commandName);343 344                List<string> commandsToPostFilter = new List<string>();345 346                Action<string> onEachCommand = importedCommandName => commandsToPostFilter.Add(importedCommandName);347 348                // Process any exports from the module that we determine from349                // the -Function, -Cmdlet, or -Alias parameters350                ProcessCmdletArguments(boundParameters["Function"], onEachCommand);351                ProcessCmdletArguments(boundParameters["Cmdlet"], onEachCommand);352                ProcessCmdletArguments(boundParameters["Alias"], onEachCommand);353 354                // Now, go through all of the discovered modules on Import-Module355                // and register them for deeper investigation.356                Action<string> onEachModule = moduleName =>357                {358                    ModuleIntrinsics.Tracer.WriteLine("Discovered module import: {0}", moduleName);359                    DiscoveredModules.Add(360                        new RequiredModuleInfo361                        {362                            Name = moduleName,363                            CommandsToPostFilter = commandsToPostFilter364                        });365                };366                ProcessCmdletArguments(boundParameters["Name"], onEachModule);367 368                return AstVisitAction.SkipChildren;369            }370 371            // They are exporting a module member372            if (string.Equals(commandName, "Export-ModuleMember", StringComparison.OrdinalIgnoreCase) ||373                string.Equals(commandName, "Microsoft.PowerShell.Core\\Export-ModuleMember", StringComparison.OrdinalIgnoreCase) ||374                string.Equals(commandName, "$script:ExportModuleMember", StringComparison.OrdinalIgnoreCase))375            {376                // Export-ModuleMember *377                // Export-ModuleMember Exported-UnNamedModuleMember378                // Export-ModuleMember -Function Exported-FunctionModuleMember1, Exported-FunctionModuleMember2 -Cmdlet Exported-CmdletModuleMember `379                //    -Alias Exported-AliasModuleMember380                // & $script:ExportModuleMember -Function (...)381 382                var boundParameters = DoPsuedoParameterBinding(commandAst, commandName);383 384                Action<string> onEachFunction = exportedCommandName =>385                {386                    DiscoveredCommandFilters.Add(exportedCommandName);387                    ModuleIntrinsics.Tracer.WriteLine("Discovered explicit export: {0}", exportedCommandName);388 389                    // If the export doesn't contain wildcards, then add it to the390                    // discovered commands as well. It is likely that they created391                    // the command dynamically392                    if ((!WildcardPattern.ContainsWildcardCharacters(exportedCommandName)) &&393                        (!DiscoveredExports.Contains(exportedCommandName)))394                    {395                        DiscoveredExports.Add(exportedCommandName);396                    }397                };398                ProcessCmdletArguments(boundParameters["Function"], onEachFunction);399                ProcessCmdletArguments(boundParameters["Cmdlet"], onEachFunction);400 401                Action<string> onEachAlias = exportedAlias =>402                {403                    DiscoveredCommandFilters.Add(exportedAlias);404 405                    // If the export doesn't contain wildcards, then add it to the406                    // discovered commands as well. It is likely that they created407                    // the command dynamically408                    if (!WildcardPattern.ContainsWildcardCharacters(exportedAlias))409                    {410                        DiscoveredAliases[exportedAlias] = null;411                    }412                };413                ProcessCmdletArguments(boundParameters["Alias"], onEachAlias);414 415                return AstVisitAction.SkipChildren;416            }417 418            // They are exporting a module member using our advanced 'public' function419            // that we've presented in many demos420            if ((string.Equals(commandName, "public", StringComparison.OrdinalIgnoreCase)) &&421                (commandAst.CommandElements.Count > 2))422            {423                // public function Publicly-ExportedFunction424                // public alias Publicly-ExportedAlias425                string publicCommandName = commandAst.CommandElements[2].ToString().Trim();426                DiscoveredExports.Add(publicCommandName);427                DiscoveredCommandFilters.Add(publicCommandName);428            }429 430            return AstVisitAction.SkipChildren;431        }432 433        private static void ProcessCmdletArguments(object value, Action<string> onEachArgument)434        {435            if (value == null)436            {437                return;438            }439 440            var commandName = value as string;441            if (commandName != null)442            {443                onEachArgument(commandName);444                return;445            }446 447            var names = value as object[];448            if (names != null)449            {450                foreach (var n in names)451                {452                    // This is slightly more permissive than what would really happen with parameter binding453                    // in that it would allow arrays of arrays in ways that don't actually work454                    ProcessCmdletArguments(n, onEachArgument);455                }456            }457        }458 459        // This method does parameter binding for a very limited set of scenarios, specifically460        // for New-Alias, Set-Alias, Import-Module, and Export-ModuleMember.  It might not even461        // correctly handle these cmdlets if new parameters are added.462        //463        // It also only populates the bound parameters for a limited set of parameters needed464        // for module analysis.465        private static Hashtable DoPsuedoParameterBinding(CommandAst commandAst, string commandName)466        {467            var result = new Hashtable(StringComparer.OrdinalIgnoreCase);468 469            var parameterBindingInfo = s_parameterBindingInfoTable[commandName].parameterInfo;470 471            int positionsBound = 0;472 473            for (int i = 1; i < commandAst.CommandElements.Count; i++)474            {475                var element = commandAst.CommandElements[i];476                var specifiedParameter = element as CommandParameterAst;477                if (specifiedParameter != null)478                {479                    bool boundParameter = false;480                    var specifiedParamName = specifiedParameter.ParameterName;481                    foreach (var parameterInfo in parameterBindingInfo)482                    {483                        if (parameterInfo.name.StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase))484                        {485                            if (parameterInfo.position != -1)486                            {487                                positionsBound |= 1 << parameterInfo.position;488                            }489 490                            var argumentAst = specifiedParameter.Argument;491                            if (argumentAst == null)492                            {493                                argumentAst = commandAst.CommandElements[i] as ExpressionAst;494                                if (argumentAst != null)495                                {496                                    i += 1;497                                }498                            }499 500                            if (argumentAst != null)501                            {502                                boundParameter = true;503                                result[parameterInfo.name] =504                                    GetSafeValueVisitor.GetSafeValue(argumentAst, null, GetSafeValueVisitor.SafeValueContext.ModuleAnalysis);505                            }506 507                            break;508                        }509                    }510 511                    if (boundParameter || specifiedParameter.Argument != null)512                    {513                        continue;514                    }515 516                    if (!"PassThru".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&517                        !"Force".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&518                        !"Confirm".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&519                        !"Global".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&520                        !"AsCustomObject".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&521                        !"Verbose".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&522                        !"Debug".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&523                        !"DisableNameChecking".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase) &&524                        !"NoClobber".StartsWith(specifiedParamName, StringComparison.OrdinalIgnoreCase))525                    {526                        // Named parameter, skip the argument (except for specific switch parameters527                        i += 1;528                    }529                }530                else531                {532                    // Positional argument, find which position we want to bind533                    int pos = 0;534                    for (; pos < 10; pos++)535                    {536                        if ((positionsBound & (1 << pos)) == 0)537                            break;538                    }539 540                    positionsBound |= 1 << pos;541 542                    // Now see if we care (we probably do, but if the user did something odd, like specify too many, then we don't really)543                    foreach (var parameterInfo in parameterBindingInfo)544                    {545                        if (parameterInfo.position == pos)546                        {547                            result[parameterInfo.name] = GetSafeValueVisitor.GetSafeValue(548                                commandAst.CommandElements[i], null,549                                GetSafeValueVisitor.SafeValueContext.ModuleAnalysis);550                        }551                    }552                }553            }554 555            return result;556        }557 558        private static readonly Dictionary<string, ParameterBindingInfo> s_parameterBindingInfoTable;559 560        private sealed class ParameterBindingInfo561        {562            internal ParameterInfo[] parameterInfo;563        }564 565        private struct ParameterInfo566        {567            internal string name;568            internal int position;569        }570    }571 572    // Class to keep track of modules we need to import, and commands that should573    // be filtered out of them.574    internal class RequiredModuleInfo575    {576        internal string Name { get; set; }577 578        internal List<string> CommandsToPostFilter { get; set; }579    }580}581