Skip to content

Commit 2135a17

Browse files
author
Kapil Borle
authored
Merge pull request #589 from PowerShell/kapilmb/FixPsd1FalseAlarm
Fix false positives on PSD1 files
2 parents 635008e + 09d86df commit 2135a17

11 files changed

+263
-14
lines changed

Engine/Helper.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class Helper
3434
private CommandInvocationIntrinsics invokeCommand;
3535
private IOutputWriter outputWriter;
3636
private Object getCommandLock = new object();
37+
private readonly static Version minSupportedPSVersion = new Version(3, 0);
3738

3839
#endregion
3940

@@ -1602,6 +1603,182 @@ public bool GetNamedArgumentAttributeValue(NamedAttributeArgumentAst namedAttrib
16021603
return false;
16031604
}
16041605

1606+
/// <summary>
1607+
/// Gets valid keys of a PowerShell module manifest file for a given PowerShell version
1608+
/// </summary>
1609+
/// <param name="powershellVersion">Version parameter; valid if >= 3.0</param>
1610+
/// <returns>Returns an enumerator over valid keys</returns>
1611+
public static IEnumerable<string> GetModuleManifestKeys(Version powershellVersion)
1612+
{
1613+
if (powershellVersion == null)
1614+
{
1615+
throw new ArgumentNullException("powershellVersion");
1616+
}
1617+
if (!IsPowerShellVersionSupported(powershellVersion))
1618+
{
1619+
throw new ArgumentException("Invalid PowerShell version. Choose from version greater than or equal to 3.0");
1620+
}
1621+
var keys = new List<string>();
1622+
var keysCommon = new List<string> {
1623+
"RootModule",
1624+
"ModuleVersion",
1625+
"GUID",
1626+
"Author",
1627+
"CompanyName",
1628+
"Copyright",
1629+
"Description",
1630+
"PowerShellVersion",
1631+
"PowerShellHostName",
1632+
"PowerShellHostVersion",
1633+
"DotNetFrameworkVersion",
1634+
"CLRVersion",
1635+
"ProcessorArchitecture",
1636+
"RequiredModules",
1637+
"RequiredAssemblies",
1638+
"ScriptsToProcess",
1639+
"TypesToProcess",
1640+
"FormatsToProcess",
1641+
"NestedModules",
1642+
"FunctionsToExport",
1643+
"CmdletsToExport",
1644+
"VariablesToExport",
1645+
"AliasesToExport",
1646+
"ModuleList",
1647+
"FileList",
1648+
"PrivateData",
1649+
"HelpInfoURI",
1650+
"DefaultCommandPrefix"};
1651+
keys.AddRange(keysCommon);
1652+
if (powershellVersion.Major >= 5)
1653+
{
1654+
keys.Add("DscResourcesToExport");
1655+
}
1656+
if (powershellVersion >= new Version(5, 1))
1657+
{
1658+
keys.Add("CompatiblePSEditions");
1659+
}
1660+
return keys;
1661+
}
1662+
1663+
/// <summary>
1664+
/// Gets deprecated keys of PowerShell module manifest
1665+
/// </summary>
1666+
/// <returns>Returns an enumerator over deprecated keys</returns>
1667+
public static IEnumerable<string> GetDeprecatedModuleManifestKeys()
1668+
{
1669+
return new List<string> { "ModuleToProcess" };
1670+
}
1671+
1672+
/// <summary>
1673+
/// Get a mapping between string type keys and StatementAsts from module manifest hashtable ast
1674+
///
1675+
/// This is a workaround as SafeGetValue is not supported on PS v5 and below.
1676+
/// </summary>
1677+
/// <param name="hast">Hashtable Ast obtained from module manifest</param>
1678+
/// <returns>A dictionary that maps string keys to values of StatementAst type</returns>
1679+
private static Dictionary<string, StatementAst> GetMapFromHashtableAst(HashtableAst hast)
1680+
{
1681+
var map = new Dictionary<string, StatementAst>(StringComparer.OrdinalIgnoreCase);
1682+
foreach (var pair in hast.KeyValuePairs)
1683+
{
1684+
var key = pair.Item1 as StringConstantExpressionAst;
1685+
if (key == null)
1686+
{
1687+
return null;
1688+
}
1689+
map[key.Value] = pair.Item2;
1690+
}
1691+
return map;
1692+
}
1693+
1694+
/// <summary>
1695+
/// Checks if the version is supported
1696+
///
1697+
/// PowerShell versions with Major greater than 3 are supported
1698+
/// </summary>
1699+
/// <param name="version">PowerShell version</param>
1700+
/// <returns>true if the given version is supported else false</returns>
1701+
public static bool IsPowerShellVersionSupported(Version version)
1702+
{
1703+
if (version == null)
1704+
{
1705+
throw new ArgumentNullException("version");
1706+
}
1707+
return version >= minSupportedPSVersion;
1708+
}
1709+
1710+
/// <summary>
1711+
/// Checks if a given file is a valid PowerShell module manifest
1712+
/// </summary>
1713+
/// <param name="filepath">Path to module manifest</param>
1714+
/// <param name="powershellVersion">Version parameter; valid if >= 3.0</param>
1715+
/// <returns>true if given filepath points to a module manifest, otherwise false</returns>
1716+
public static bool IsModuleManifest(string filepath, Version powershellVersion = null)
1717+
{
1718+
Token[] tokens;
1719+
ParseError[] errors;
1720+
if (filepath == null)
1721+
{
1722+
throw new ArgumentNullException("filepath");
1723+
}
1724+
if (powershellVersion != null
1725+
&& !IsPowerShellVersionSupported(powershellVersion))
1726+
{
1727+
return false;
1728+
}
1729+
if (!Path.GetExtension(filepath).Equals(".psd1", StringComparison.OrdinalIgnoreCase))
1730+
{
1731+
return false;
1732+
}
1733+
1734+
//using parsefile causes the parser to crash!
1735+
string fileContent = File.ReadAllText(filepath);
1736+
var ast = Parser.ParseInput(fileContent, out tokens, out errors);
1737+
var hast = ast.Find(x => x is HashtableAst, false) as HashtableAst;
1738+
if (hast == null)
1739+
{
1740+
return false;
1741+
}
1742+
var map = GetMapFromHashtableAst(hast);
1743+
var deprecatedKeys = GetDeprecatedModuleManifestKeys();
1744+
IEnumerable<string> allKeys;
1745+
if (powershellVersion != null)
1746+
{
1747+
allKeys = GetModuleManifestKeys(powershellVersion);
1748+
}
1749+
else
1750+
{
1751+
Version version = null;
1752+
if (map.ContainsKey("PowerShellVersion"))
1753+
{
1754+
var versionStrAst = map["PowerShellVersion"].Find(x => x is StringConstantExpressionAst, false);
1755+
if (versionStrAst != null)
1756+
{
1757+
try
1758+
{
1759+
version = new Version((versionStrAst as StringConstantExpressionAst).Value);
1760+
}
1761+
catch
1762+
{
1763+
// we just ignore if the value is not a valid version
1764+
}
1765+
}
1766+
}
1767+
if (version != null
1768+
&& IsPowerShellVersionSupported(version))
1769+
{
1770+
allKeys = GetModuleManifestKeys(version);
1771+
}
1772+
else
1773+
{
1774+
// default to version 5.1
1775+
allKeys = GetModuleManifestKeys(new Version("5.1"));
1776+
}
1777+
}
1778+
1779+
// check if the keys given in module manifest are a proper subset of Keys
1780+
return map.Keys.All(x => allKeys.Concat(deprecatedKeys).Contains(x, StringComparer.OrdinalIgnoreCase));
1781+
}
16051782
#endregion Methods
16061783
}
16071784

Rules/AvoidUsingDeprecatedManifestFields.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
3939
{
4040
throw new ArgumentNullException(Strings.NullAstErrorMessage);
4141
}
42-
43-
if (String.Equals(System.IO.Path.GetExtension(fileName), ".psd1", StringComparison.OrdinalIgnoreCase))
42+
if (fileName == null)
43+
{
44+
yield break;
45+
}
46+
if (Helper.IsModuleManifest(fileName))
4447
{
4548
var ps = System.Management.Automation.PowerShell.Create();
4649
IEnumerable<PSObject> result = null;

Rules/MissingModuleManifestField.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ public class MissingModuleManifestField : IScriptRule
3535
/// <returns>A List of diagnostic results of this rule</returns>
3636
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
3737
{
38-
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
39-
40-
if (String.Equals(System.IO.Path.GetExtension(fileName), ".psd1", StringComparison.OrdinalIgnoreCase))
38+
if (ast == null)
39+
{
40+
throw new ArgumentNullException(Strings.NullAstErrorMessage);
41+
}
42+
if (fileName == null)
43+
{
44+
yield break;
45+
}
46+
if (Helper.IsModuleManifest(fileName))
4147
{
4248
IEnumerable<ErrorRecord> errorRecords;
4349
var psModuleInfo = Helper.Instance.GetModuleManifest(fileName, out errorRecords);

Rules/UseToExportFieldsInManifest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
5050
throw new ArgumentNullException(Strings.NullAstErrorMessage);
5151
}
5252

53-
if (fileName == null || !fileName.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase))
53+
if (fileName == null || !Helper.IsModuleManifest(fileName))
5454
{
5555
yield break;
5656
}

Tests/Rules/AvoidUnloadableModuleOrMissingRequiredFieldInManifest.tests.ps1

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,19 @@ Describe "MissingRequiredFieldModuleManifest" {
2525
$violations.Message | Should Match $missingMessage
2626
}
2727

28-
$numExpectedCorrections = 1
29-
It "has $numExpectedCorrections suggested corrections" {
30-
$violations.SuggestedCorrections.Count | Should Be $numExpectedCorrections
31-
}
28+
$numExpectedCorrections = 1
29+
It "has $numExpectedCorrections suggested corrections" {
30+
$violations.SuggestedCorrections.Count | Should Be $numExpectedCorrections
31+
}
3232

3333

34-
It "has the right suggested correction" {
35-
$expectedText = @'
34+
It "has the right suggested correction" {
35+
$expectedText = @'
3636
# Version number of this module.
3737
ModuleVersion = '1.0.0.0'
3838
'@
39-
$violations[0].SuggestedCorrections[0].Text | Should Match $expectedText
40-
Get-ExtentText $violations[0].SuggestedCorrections[0] $violationFilepath | Should Match ""
39+
$violations[0].SuggestedCorrections[0].Text | Should Match $expectedText
40+
Get-ExtentText $violations[0].SuggestedCorrections[0] $violationFilepath | Should Match ""
4141
}
4242
}
4343

@@ -52,5 +52,47 @@ ModuleVersion = '1.0.0.0'
5252
{Invoke-ScriptAnalyzer -Path $noHashtableFilepath -IncludeRule $missingMemberRuleName} | Should Not Throw
5353
}
5454
}
55+
56+
Context "Validate the contents of a .psd1 file" {
57+
It "detects a valid module manifest file" {
58+
$filepath = Join-Path $directory "TestManifest/ManifestGood.psd1"
59+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Helper]::IsModuleManifest($filepath, [version]"5.0") | Should Be $true
60+
}
61+
62+
It "detects a .psd1 file which is not module manifest" {
63+
$filepath = Join-Path $directory "TestManifest/PowerShellDataFile.psd1"
64+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Helper]::IsModuleManifest($filepath, [version]"5.0") | Should Be $false
65+
}
66+
67+
It "detects valid module manifest file for PSv5" {
68+
$filepath = Join-Path $directory "TestManifest/ManifestGoodPSv5.psd1"
69+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Helper]::IsModuleManifest($filepath, [version]"5.0") | Should Be $true
70+
}
71+
72+
It "does not validate PSv5 module manifest file for PSv3 check" {
73+
$filepath = Join-Path $directory "TestManifest/ManifestGoodPSv5.psd1"
74+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Helper]::IsModuleManifest($filepath, [version]"3.0") | Should Be $false
75+
}
76+
77+
It "detects valid module manifest file for PSv4" {
78+
$filepath = Join-Path $directory "TestManifest/ManifestGoodPSv4.psd1"
79+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Helper]::IsModuleManifest($filepath, [version]"4.0") | Should Be $true
80+
}
81+
82+
It "detects valid module manifest file for PSv3" {
83+
$filepath = Join-Path $directory "TestManifest/ManifestGoodPSv3.psd1"
84+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Helper]::IsModuleManifest($filepath, [version]"3.0") | Should Be $true
85+
}
86+
}
87+
88+
Context "When given a non module manifest file" {
89+
It "does not flag a PowerShell data file" {
90+
Invoke-ScriptAnalyzer `
91+
-Path "$directory/TestManifest/PowerShellDataFile.psd1" `
92+
-IncludeRule "PSMissingModuleManifestField" `
93+
-OutVariable ruleViolation
94+
$ruleViolation.Count | Should Be 0
95+
}
96+
}
5597
}
5698

Tests/Rules/AvoidUsingDeprecatedManifestFields.tests.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,14 @@ Describe "AvoidUsingDeprecatedManifestFields" {
2121
$noViolations.Count | Should Be 0
2222
}
2323
}
24+
25+
Context "When given a non module manifest file" {
26+
It "does not flag a PowerShell data file" {
27+
Invoke-ScriptAnalyzer `
28+
-Path "$directory/TestManifest/PowerShellDataFile.psd1" `
29+
-IncludeRule "PSAvoidUsingDeprecatedManifestFields" `
30+
-OutVariable ruleViolation
31+
$ruleViolation.Count | Should Be 0
32+
}
33+
}
2434
}
5.06 KB
Binary file not shown.
5.07 KB
Binary file not shown.
6.41 KB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@{"a"=1; "b"=2}

Tests/Rules/UseToExportFieldsInManifest.tests.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ Describe "UseManifestExportFields" {
9999
$results.Count | Should be 0
100100
}
101101
}
102+
103+
Context "When given a non module manifest file" {
104+
It "does not flag a PowerShell data file" {
105+
Invoke-ScriptAnalyzer `
106+
-Path "$directory/TestManifest/PowerShellDataFile.psd1" `
107+
-IncludeRule "PSUseToExportFieldsInManifest" `
108+
-OutVariable ruleViolation
109+
$ruleViolation.Count | Should Be 0
110+
}
111+
}
102112
}
103113

104114

0 commit comments

Comments
 (0)