MegaBites-AI/Windows-powershell
0372
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 