-
Notifications
You must be signed in to change notification settings - Fork 407
Add UseSingleValueFromPipelineParameter Rule #2119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
liamjpeters
wants to merge
7
commits into
PowerShell:main
Choose a base branch
from
liamjpeters:UseSingleValueFromPipelineParameter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+638
−1
Open
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
de338e0
Added Rule UseSingleValueFromPipelineParameter
liamjpeters 9b6cbaf
Correct rule name for consistency and to fix failing docs tests
liamjpeters 834059d
Update handling of empty AST passed to AnalyzeScript
liamjpeters f2266aa
Fixup built-in rule count test
liamjpeters 14a6c8e
Make rule not enabled by default
liamjpeters d1e5564
Merge branch 'main' into UseSingleValueFromPipelineParameter
bergmeister d77965e
Apply suggestions from code review
liamjpeters File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Globalization; | ||
| using System.Linq; | ||
| using System.Management.Automation.Language; | ||
| #if !CORECLR | ||
| using System.ComponentModel.Composition; | ||
| #endif | ||
|
|
||
| namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules | ||
| { | ||
| #if !CORECLR | ||
| [Export(typeof(IScriptRule))] | ||
| #endif | ||
|
|
||
| /// <summary> | ||
| /// Rule that identifies parameter blocks with multiple parameters in | ||
| /// the same parameter set that are marked as ValueFromPipeline=true, which | ||
| /// can cause undefined behavior. | ||
| /// </summary> | ||
| public class UseSingleValueFromPipelineParameter : ConfigurableRule | ||
| { | ||
| private const string AllParameterSetsName = "__AllParameterSets"; | ||
|
|
||
| /// <summary> | ||
| /// Analyzes the PowerShell AST for parameter sets with multiple ValueFromPipeline parameters. | ||
| /// </summary> | ||
| /// <param name="ast">The PowerShell Abstract Syntax Tree to analyze.</param> | ||
| /// <param name="fileName">The name of the file being analyzed (for diagnostic reporting).</param> | ||
| /// <returns>A collection of diagnostic records for each violating parameter.</returns> | ||
| public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName) | ||
| { | ||
| if (ast == null) | ||
| { | ||
| throw new ArgumentNullException(Strings.NullAstErrorMessage); | ||
| } | ||
| // Find all param blocks that have a Parameter attribute with | ||
| // ValueFromPipeline set to true. | ||
| var paramBlocks = ast.FindAll(testAst => testAst is ParamBlockAst, true) | ||
| .Where(paramBlock => paramBlock.FindAll( | ||
| attributeAst => attributeAst is AttributeAst attr && | ||
| ParameterAttributeAstHasValueFromPipeline(attr), | ||
| true | ||
| ).Any()); | ||
|
|
||
| foreach (var paramBlock in paramBlocks) | ||
| { | ||
| // Find all parameter declarations in the current param block | ||
| // Convert the generic ast objects into ParameterAst Objects | ||
| // For each ParameterAst, find all it's attributes that have | ||
| // ValueFromPipeline set to true (either explicitly or | ||
| // implicitly). Flatten the results into a single collection of | ||
| // Annonymous objects relating the parameter with it's attribute | ||
| // and then group them by parameter set name. | ||
| // | ||
| // | ||
| // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parameter_sets?#reserved-parameter-set-name | ||
| // | ||
| // The default parameter set name is '__AllParameterSets'. | ||
| // Not specifying a parameter set name and using the parameter | ||
| // set name '__AllParameterSets' are equivalent, so we shouldn't | ||
| // treat them like they're different just because one is an | ||
| // empty string and the other is not. | ||
| // | ||
| // Filter the list to only keep parameter sets that have more | ||
| // than one ValueFromPipeline parameter. | ||
| var parameterSetGroups = paramBlock.FindAll(n => n is ParameterAst, true) | ||
| .Cast<ParameterAst>() | ||
| .SelectMany(parameter => parameter.FindAll( | ||
| a => a is AttributeAst attr && ParameterAttributeAstHasValueFromPipeline(attr), | ||
| true | ||
| ).Cast<AttributeAst>().Select(attr => new { Parameter = parameter, Attribute = attr })) | ||
| .GroupBy(item => GetParameterSetForAttribute(item.Attribute) ?? AllParameterSetsName) | ||
| .Where(group => group.Count() > 1); | ||
|
|
||
|
|
||
| foreach (var group in parameterSetGroups) | ||
| { | ||
| // __AllParameterSets being the default name is...obscure. | ||
| // Instead we'll show the user "default". It's more than | ||
| // likely the user has not specified a parameter set name, | ||
| // so default will make sense. If they have used 'default' | ||
| // as their parameter set name, then we're still correct. | ||
| var parameterSetName = group.Key == AllParameterSetsName ? "default" : group.Key; | ||
|
|
||
| // Create a concatenated string of parameter names that | ||
| // conflict in this parameter set | ||
| var parameterNames = string.Join(", ", group.Select(item => item.Parameter.Name.VariablePath.UserPath)); | ||
|
|
||
| // We emit a diagnostic record for each offending parameter | ||
| // attribute in the parameter set so it's obvious where all the | ||
| // occurrences are. | ||
| foreach (var item in group) | ||
| { | ||
| var message = string.Format(CultureInfo.CurrentCulture, | ||
| Strings.UseSingleValueFromPipelineParameterError, | ||
| parameterNames, | ||
| parameterSetName); | ||
|
|
||
| yield return new DiagnosticRecord( | ||
| message, | ||
| item.Attribute.Extent, | ||
| GetName(), | ||
| DiagnosticSeverity.Warning, | ||
| fileName, | ||
| parameterSetName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns whether the specified AttributeAst represents a Parameter attribute | ||
| /// that has the ValueFromPipeline named argument set to true (either explicitly or | ||
| /// implicitly). | ||
| /// </summary> | ||
| /// <param name="attributeAst">The Parameter attribute to examine.</param> | ||
| /// <returns>Whether the attribute has the ValueFromPipeline named argument set to true.</returns> | ||
| private static bool ParameterAttributeAstHasValueFromPipeline(AttributeAst attributeAst) | ||
| { | ||
| // Exit quickly if the attribute is null, has no named arguments, or | ||
| // is not a parameter attribute. | ||
| if (attributeAst?.NamedArguments == null || | ||
| !string.Equals(attributeAst.TypeName?.Name, "Parameter", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| return attributeAst.NamedArguments | ||
| .OfType<NamedAttributeArgumentAst>() | ||
| .Any(namedArg => string.Equals( | ||
| namedArg?.ArgumentName, | ||
| "ValueFromPipeline", | ||
| StringComparison.OrdinalIgnoreCase | ||
| // Helper.Instance.GetNamedArgumentAttributeValue handles both explicit ($true) | ||
| // and implicit (no value specified) ValueFromPipeline declarations | ||
| ) && Helper.Instance.GetNamedArgumentAttributeValue(namedArg)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the ParameterSetName value from a Parameter attribute. | ||
| /// </summary> | ||
| /// <param name="attributeAst">The Parameter attribute to examine.</param> | ||
| /// <returns>The parameter set name, or null if not found or empty.</returns> | ||
| private static string GetParameterSetForAttribute(AttributeAst attributeAst) | ||
| { | ||
| // Exit quickly if the attribute is null, has no named arguments, or | ||
| // is not a parameter attribute. | ||
| if (attributeAst?.NamedArguments == null || | ||
| !string.Equals(attributeAst.TypeName.Name, "Parameter", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return attributeAst.NamedArguments | ||
| .OfType<NamedAttributeArgumentAst>() | ||
| .Where(namedArg => string.Equals( | ||
| namedArg?.ArgumentName, | ||
| "ParameterSetName", | ||
| StringComparison.OrdinalIgnoreCase | ||
| )) | ||
| .Select(namedArg => namedArg?.Argument) | ||
| .OfType<StringConstantExpressionAst>() | ||
| .Select(stringConstAst => stringConstAst?.Value) | ||
| .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); | ||
| } | ||
|
|
||
| public override string GetCommonName() => Strings.UseSingleValueFromPipelineParameterCommonName; | ||
|
|
||
| public override string GetDescription() => Strings.UseSingleValueFromPipelineParameterDescription; | ||
|
|
||
| public override string GetName() => string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.NameSpaceFormat, | ||
| GetSourceName(), | ||
| Strings.UseSingleValueFromPipelineParameterName); | ||
|
|
||
| public override RuleSeverity GetSeverity() => RuleSeverity.Warning; | ||
|
|
||
| public override string GetSourceName() => Strings.SourceName; | ||
|
|
||
| public override SourceType GetSourceType() => SourceType.Builtin; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.