Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,29 +60,29 @@ private static FormatterResult[] FormatPowerShellTree(
psd1Files = psd1Files.Where(p => !string.Equals(p, manifestPath, StringComparison.OrdinalIgnoreCase)).ToArray();
}

var results = new List<FormatterResult>(all.Length + 4);
var batches = new List<FormattingBatch>();

// Legacy PSPublishModule defaults did not format standalone PS1 files unless explicitly configured.
var standardPs1 = cfg.Standard.FormatCodePS1;
if (standardPs1?.Enabled == true && ps1Files.Length > 0)
results.AddRange(pipeline.Run(ps1Files, BuildFormatOptions(standardPs1)));
batches.Add(new FormattingBatch(ps1Files, BuildFormatOptions(standardPs1)));

if (cfg.Standard.FormatCodePSM1?.Enabled == true && psm1Files.Length > 0)
results.AddRange(pipeline.Run(psm1Files, BuildFormatOptions(cfg.Standard.FormatCodePSM1)));
batches.Add(new FormattingBatch(psm1Files, BuildFormatOptions(cfg.Standard.FormatCodePSM1)));

if (standardPsd1?.Enabled == true && psd1Files.Length > 0)
results.AddRange(pipeline.Run(psd1Files, BuildFormatOptions(standardPsd1)));
batches.Add(new FormattingBatch(psd1Files, BuildFormatOptions(standardPsd1)));

if (includeMergeFormatting)
{
if (cfg.Merge.FormatCodePSM1?.Enabled == true && File.Exists(rootPsm1))
results.AddRange(pipeline.Run(new[] { rootPsm1 }, BuildFormatOptions(cfg.Merge.FormatCodePSM1)));
batches.Add(new FormattingBatch(new[] { rootPsm1 }, BuildFormatOptions(cfg.Merge.FormatCodePSM1)));

if (mergePsd1?.Enabled == true && File.Exists(manifestPath))
results.AddRange(pipeline.Run(new[] { manifestPath }, BuildFormatOptions(mergePsd1)));
batches.Add(new FormattingBatch(new[] { manifestPath }, BuildFormatOptions(mergePsd1)));
}

return results.ToArray();
return pipeline.RunBatches(batches).ToArray();

static bool IsPowerShellSourceFile(string path)
=> HasExtension(path, ".ps1", ".psm1", ".psd1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ private void ExecuteFormattingAndSigningPhases(
if (plan.Formatting is not null && !IsReusingCompiledPowerShellModule(plan))
{
var formattingPipeline = new FormattingPipeline(_logger);
using var formatterSession = formattingPipeline.BeginSession();

session.Start(session.FormatStagingStep);
try
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Management.Automation.Language;

namespace PowerForge;

internal sealed partial class NestedFunctionVisibilityAnalyzer
{
private readonly Dictionary<CommandAst, StaticBindingResult> _commandBindings = new();

private StaticBindingResult BindCommandCached(CommandAst command)
{
if (_commandBindings.TryGetValue(command, out var binding))
return binding;

binding = StaticParameterBinder.BindCommand(command);
// Incomplete metadata can become available through module discovery later in this analysis.
// Only retain successful bindings, and never share them between analyzed syntax trees.
if (binding.BindingExceptions.Count == 0)
_commandBindings.Add(command, binding);

return binding;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ assignment.Left is VariableExpressionAst variable &&
NormalizeInvocationName(rawConsumerName));
}

private static bool IsFunctionLookupCommand(CommandAst command, string functionName)
private bool IsFunctionLookupCommand(CommandAst command, string functionName)
{
var commandName = NormalizeInvocationName(command.GetCommandName());
if (string.Equals(commandName, "Get-Command", StringComparison.OrdinalIgnoreCase) ||
Expand Down Expand Up @@ -151,11 +151,11 @@ private static bool IsFunctionLookupCommand(CommandAst command, string functionN
"LiteralPath");
}

private static bool GetCommandCanReturnFunction(CommandAst command)
private bool GetCommandCanReturnFunction(CommandAst command)
{
try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
if (!binding.BoundParameters.TryGetValue("CommandType", out var result))
return true;

Expand All @@ -177,7 +177,7 @@ private static bool GetCommandCanReturnFunction(CommandAst command)
}
}

private static bool BoundLookupCanMatchFunction(
private bool BoundLookupCanMatchFunction(
CommandAst command,
string functionName,
bool providerPath,
Expand All @@ -186,7 +186,7 @@ private static bool BoundLookupCanMatchFunction(
{
try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
foreach (var parameterName in parameterNames)
{
if (binding.BoundParameters.TryGetValue(parameterName, out var result))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Language;

namespace PowerForge;

internal sealed partial class NestedFunctionVisibilityAnalyzer
{
private static bool IsWithinExecutionScope(CommandAst command, ScriptBlockAst scope)
{
for (Ast? current = command.Parent; current is not null; current = current.Parent)
{
if (ReferenceEquals(current, scope))
return true;
if (current is FunctionDefinitionAst)
return false;
}

return false;
}

private sealed class ScopeExecutionGraph
{
private readonly Dictionary<FunctionDefinitionAst, InvocationSite[]> _functionInvocations = new();

internal ScopeExecutionGraph(NestedFunctionVisibilityAnalyzer analyzer, ScriptBlockAst scope)
{
var functions = scope.FindAll(
ast => ast is FunctionDefinitionAst,
searchNestedScriptBlocks: true)
.Cast<FunctionDefinitionAst>()
.Select(function => new
{
Function = function,
HasContext = analyzer.TryGetPotentialDeclarationContext(function, out var declarationScope, out _),
Scope = declarationScope
})
.Where(item => item.HasContext && ReferenceEquals(item.Scope, scope))
.Select(item => item.Function)
.ToArray();

FunctionsByName = functions
.GroupBy(
function => NormalizeDeclaredFunctionName(function.Name),
StringComparer.OrdinalIgnoreCase)
.ToDictionary(
group => group.Key,
group => group.ToArray(),
StringComparer.OrdinalIgnoreCase);

RootInvocations = scope.FindAll(
ast => ast is CommandAst,
searchNestedScriptBlocks: true)
.Cast<CommandAst>()
.Where(command => IsWithinExecutionScope(command, scope))
.Select(command => new InvocationSite(command, FindContainingTrap(command, scope)))
.ToArray();

foreach (var function in functions)
{
_functionInvocations[function] = function.Body.FindAll(
ast => ast is CommandAst,
searchNestedScriptBlocks: true)
.Cast<CommandAst>()
.Where(command => IsWithinExecutionScope(command, function.Body))
.Select(command => new InvocationSite(command, trap: null))
.ToArray();
}
}

internal IReadOnlyDictionary<string, FunctionDefinitionAst[]> FunctionsByName { get; }
internal InvocationSite[] RootInvocations { get; }

internal InvocationSite[] GetFunctionInvocations(FunctionDefinitionAst function)
{
return _functionInvocations.TryGetValue(function, out var invocations)
? invocations
: Array.Empty<InvocationSite>();
}
}

private sealed class InvocationSite
{
internal InvocationSite(CommandAst command, TrapStatementAst? trap)
{
Command = command;
Trap = trap;
var commandName = command.GetCommandName();
if (string.Equals(NormalizeInvocationName(commandName), "Invoke-Expression", StringComparison.OrdinalIgnoreCase) ||
string.Equals(commandName, "iex", StringComparison.OrdinalIgnoreCase))
{
Name = TryGetInvokeExpressionTarget(command);
IsDynamic = string.IsNullOrWhiteSpace(Name);
}
else
{
Name = commandName;
IsDynamic = string.IsNullOrWhiteSpace(Name) &&
command.CommandElements.Count > 0 &&
command.CommandElements[0] is not ScriptBlockExpressionAst;
}
}

private static string? TryGetInvokeExpressionTarget(CommandAst command)
{
var arguments = command.CommandElements.Skip(1).ToArray();
if (arguments.Length != 1 || arguments[0] is not StringConstantExpressionAst literal)
return null;

var parsed = Parser.ParseInput(literal.Value, out _, out var errors);
if (errors.Length > 0)
return null;

var commands = parsed.FindAll(ast => ast is CommandAst, searchNestedScriptBlocks: true)
.Cast<CommandAst>()
.ToArray();
return commands.Length == 1 ? commands[0].GetCommandName() : null;
}

internal CommandAst Command { get; }
internal string? Name { get; }
internal bool IsDynamic { get; }
internal TrapStatementAst? Trap { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,13 @@ private static bool IsProviderRemovalCommand(string commandName)
string.Equals(commandName, "cli", StringComparison.OrdinalIgnoreCase);
}

private static bool TryGetBoundRemovalValue(
private bool TryGetBoundRemovalValue(
CommandAst command,
IReadOnlyList<string> parameterNames,
out object? value)
=> TryGetBoundRemovalValue(command, parameterNames, out value, out _);

private static bool TryGetBoundRemovalValue(
private bool TryGetBoundRemovalValue(
CommandAst command,
IReadOnlyList<string> parameterNames,
out object? value,
Expand All @@ -175,7 +175,7 @@ private static bool TryGetBoundRemovalValue(
boundParameterName = null;
try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
foreach (var parameterName in parameterNames)
{
if (binding.BoundParameters.TryGetValue(parameterName, out var result))
Expand Down Expand Up @@ -279,11 +279,11 @@ private static bool HasDynamicValue(object? value)
return true;
}

private static bool IsPrivateAliasDeclaration(CommandAst command)
private bool IsPrivateAliasDeclaration(CommandAst command)
{
try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
if (!binding.BoundParameters.TryGetValue("Option", out var result))
return false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private bool IsPotentiallyShadowedByScriptFunction(
}
}

private static bool IsKnownSynchronousScriptBlockConsumer(
private bool IsKnownSynchronousScriptBlockConsumer(
string invocationName,
CommandAst invocation,
ScriptBlockExpressionAst scriptBlockExpression)
Expand Down Expand Up @@ -189,14 +189,14 @@ private static bool IsKnownSynchronousScriptBlockConsumer(
return false;
}

private static bool IsScriptBlockBoundToAnyParameter(
private bool IsScriptBlockBoundToAnyParameter(
CommandAst invocation,
ScriptBlockExpressionAst scriptBlockExpression,
params string[] parameterNames)
{
try
{
var binding = StaticParameterBinder.BindCommand(invocation);
var binding = BindCommandCached(invocation);
foreach (var parameterName in parameterNames)
{
if (binding.BoundParameters.TryGetValue(parameterName, out var result) &&
Expand Down Expand Up @@ -296,7 +296,7 @@ private bool IsUnshadowedAliasMutation(CommandAst command)
!IsPotentiallyShadowedByScriptFunction(command, rawCommandName, commandName);
}

private static string? TryGetAuthoredAliasName(CommandAst command)
private string? TryGetAuthoredAliasName(CommandAst command)
{
var commandName = NormalizeInvocationName(command.GetCommandName());
var aliasCommand = string.Equals(commandName, "Set-Alias", StringComparison.OrdinalIgnoreCase) ||
Expand All @@ -312,7 +312,7 @@ private bool IsUnshadowedAliasMutation(CommandAst command)

try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
if (aliasCommand && binding.BoundParameters.TryGetValue("Name", out var result))
{
return TryGetLiteralText(result.Value);
Expand All @@ -334,7 +334,7 @@ private bool IsUnshadowedAliasMutation(CommandAst command)
return null;
}

private static bool IsPotentialDynamicAliasDeclaration(CommandAst command)
private bool IsPotentialDynamicAliasDeclaration(CommandAst command)
{
var commandName = NormalizeInvocationName(command.GetCommandName());
var aliasCommand = string.Equals(commandName, "Set-Alias", StringComparison.OrdinalIgnoreCase) ||
Expand All @@ -353,7 +353,7 @@ private static bool IsPotentialDynamicAliasDeclaration(CommandAst command)

try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
if (binding.BoundParameters.TryGetValue("Path", out var result) ||
binding.BoundParameters.TryGetValue("LiteralPath", out result))
{
Expand Down Expand Up @@ -404,11 +404,11 @@ private static bool TryGetAliasProviderName(string? providerPath, out string ali
return aliasName.Length > 0;
}

private static string? TryGetAuthoredAliasTarget(CommandAst command)
private string? TryGetAuthoredAliasTarget(CommandAst command)
{
try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
if (binding.BoundParameters.TryGetValue("Value", out var result))
return TryGetLiteralText(result.Value);
}
Expand Down Expand Up @@ -501,7 +501,7 @@ private bool IsAliasRemovedBeforeInvocation(
: null;
}

private static bool HasRootAliasScope(CommandAst command)
private bool HasRootAliasScope(CommandAst command)
{
var authoredScope = TryGetAuthoredAliasScope(command);
return string.Equals(authoredScope, "Script", StringComparison.OrdinalIgnoreCase) ||
Expand Down Expand Up @@ -586,11 +586,11 @@ pipeline.Parent is not NamedBlockAst block ||
.All(statement => statement is FunctionDefinitionAst || statement is TrapStatementAst);
}

private static string? TryGetAuthoredAliasScope(CommandAst command)
private string? TryGetAuthoredAliasScope(CommandAst command)
{
try
{
var binding = StaticParameterBinder.BindCommand(command);
var binding = BindCommandCached(command);
return binding.BoundParameters.TryGetValue("Scope", out var result)
? TryGetLiteralText(result.Value)
: null;
Expand Down Expand Up @@ -733,11 +733,11 @@ private static bool HasAnyParameter(CommandAst invocation, params string[] param
return false;
}

private static bool? TryHasAnyBoundParameter(CommandAst invocation, params string[] parameterNames)
private bool? TryHasAnyBoundParameter(CommandAst invocation, params string[] parameterNames)
{
try
{
var binding = StaticParameterBinder.BindCommand(invocation);
var binding = BindCommandCached(invocation);
if (binding.BoundParameters.Keys.Any(
key => parameterNames.Any(name => string.Equals(name, key, StringComparison.OrdinalIgnoreCase))))
{
Expand Down
Loading
Loading