diff --git a/Engine/Helper.cs b/Engine/Helper.cs index 0e59ad803..16304ae86 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -2085,6 +2085,11 @@ public object VisitConvertExpression(ConvertExpressionAst convAst) { if (convAst != null) { + if (convAst.Type.TypeName.GetReflectionType() != null) + { + return convAst.Type.TypeName.GetReflectionType().FullName; + } + return convAst.Type.TypeName.FullName; } @@ -2135,6 +2140,11 @@ public object VisitTypeConstraint(TypeConstraintAst typeAst) { if (typeAst != null) { + if (typeAst.TypeName.GetReflectionType() != null) + { + return typeAst.TypeName.GetReflectionType().FullName; + } + return typeAst.TypeName.FullName; } @@ -2160,6 +2170,11 @@ public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) { if (typeExpressionAst != null) { + if (typeExpressionAst.TypeName.GetReflectionType() != null) + { + return typeExpressionAst.TypeName.GetReflectionType().FullName; + } + return typeExpressionAst.TypeName.FullName; } diff --git a/RuleDocumentation/DscExamplesPresent.md b/RuleDocumentation/DscExamplesPresent.md new file mode 100644 index 000000000..ab2743642 --- /dev/null +++ b/RuleDocumentation/DscExamplesPresent.md @@ -0,0 +1,55 @@ +#DscExamplesPresent +**Severity Level: Information** + + +##Description + +Checks that DSC examples for given resource are present. + +##How to Fix + +To fix a violation of this rule, please make sure Examples directory is present: +* For non-class based resources it should exist at the same folder level as DSCResources folder. +* For class based resources it should be present at the same folder level as resource psm1 file. + +Examples folder should contain sample configuration for given resource - file name should contain resource's name. + +##Example + +### Non-class based resource + +Let's assume we have non-class based resource with a following file structure: + +* xAzure + * DSCResources + * MSFT_xAzureSubscription + * MSFT_xAzureSubscription.psm1 + * MSFT_xAzureSubscription.schema.mof + +In this case, to fix this warning, we should add examples in a following way: + +* xAzure + * DSCResources + * MSFT_xAzureSubscription + * MSFT_xAzureSubscription.psm1 + * MSFT_xAzureSubscription.schema.mof + * Examples + * MSFT_xAzureSubscription_AddSubscriptionExample.ps1 + * MSFT_xAzureSubscription_RemoveSubscriptionExample.ps1 + +### Class based resource + +Let's assume we have class based resource with a following file structure: + +* MyDscResource + * MyDscResource.psm1 + * MyDscresource.psd1 + +In this case, to fix this warning, we should add examples in a following way: + +* MyDscResource + * MyDscResource.psm1 + * MyDscresource.psd1 + * Tests + * MyDscResource_Example1.ps1 + * MyDscResource_Example2.ps1 diff --git a/RuleDocumentation/DscTestsPresent.md b/RuleDocumentation/DscTestsPresent.md new file mode 100644 index 000000000..0ed054337 --- /dev/null +++ b/RuleDocumentation/DscTestsPresent.md @@ -0,0 +1,54 @@ +#DscTestsPresent +**Severity Level: Information** + + +##Description + +Checks that DSC tests for given resource are present. + +##How to Fix + +To fix a violation of this rule, please make sure Tests directory is present: +* For non-class based resources it should exist at the same folder level as DSCResources folder. +* For class based resources it should be present at the same folder level as resource psm1 file. + +Tests folder should contain test script for given resource - file name should contain resource's name. + +##Example + +### Non-class based resource + +Let's assume we have non-class based resource with a following file structure: + +* xAzure + * DSCResources + * MSFT_xAzureSubscription + * MSFT_xAzureSubscription.psm1 + * MSFT_xAzureSubscription.schema.mof + +In this case, to fix this warning, we should add tests in a following way: + +* xAzure + * DSCResources + * MSFT_xAzureSubscription + * MSFT_xAzureSubscription.psm1 + * MSFT_xAzureSubscription.schema.mof + * Tests + * MSFT_xAzureSubscription_Tests.ps1 + +### Class based resource + +Let's assume we have class based resource with a following file structure: + +* MyDscResource + * MyDscResource.psm1 + * MyDscresource.psd1 + +In this case, to fix this warning, we should add tests in a following way: + +* MyDscResource + * MyDscResource.psm1 + * MyDscresource.psd1 + * Tests + * MyDscResource_Tests.ps1 + diff --git a/RuleDocumentation/UseOutputTypeCorrectly.md b/RuleDocumentation/UseOutputTypeCorrectly.md new file mode 100644 index 000000000..577f430cb --- /dev/null +++ b/RuleDocumentation/UseOutputTypeCorrectly.md @@ -0,0 +1,37 @@ +#UseOutputTypeCorrectly +**Severity Level: Information** + + +##Description + +If a return type is declared, the cmdlet must return that type. If a type is returned, a return type must be declared. + +##How to Fix + +To fix a violation of this rule, please check the OuputType attribute lists and the types that are returned in your code. You can get more details by running “Get-Help about_Functions_OutputTypeAttribute” command in Windows PowerShell. + +##Example + +##Example +Wrong: + + function Get-Foo + { + [CmdletBinding()] + [OutputType([String])] + Param( + ) + return "4 + } + +Correct: + + function Get-Foo + { + [CmdletBinding()] + [OutputType([String])] + Param( + ) + + return "bar" + } diff --git a/Rules/DscExamplesPresent.cs b/Rules/DscExamplesPresent.cs new file mode 100644 index 000000000..4d3129c4c --- /dev/null +++ b/Rules/DscExamplesPresent.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Microsoft Corporation. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.Powershell.ScriptAnalyzer.Generic; +using System.ComponentModel.Composition; +using System.Globalization; +using System.IO; +using System.Management.Automation; + +namespace Microsoft.Windows.Powershell.ScriptAnalyzer.BuiltinRules +{ + /// + /// DscExamplesPresent: Checks that DSC examples for given resource are present. + /// Rule expects directory Examples to be present: + /// For non-class based resources it should exist at the same folder level as DSCResources folder. + /// For class based resources it should be present at the same folder level as resource psm1 file. + /// Examples folder should contain sample configuration for given resource - file name should contain resource's name. + /// + [Export(typeof(IDSCResourceRule))] + public class DscExamplesPresent : IDSCResourceRule + { + /// + /// AnalyzeDSCResource: Analyzes given DSC Resource + /// + /// The script's ast + /// The name of the script file being analyzed + /// The results of the analysis + public IEnumerable AnalyzeDSCResource(Ast ast, string fileName) + { + String fileNameOnly = Path.GetFileName(fileName); + String resourceName = Path.GetFileNameWithoutExtension(fileNameOnly); + String examplesQuery = String.Format("*{0}*", resourceName); + Boolean examplesPresent = false; + String expectedExamplesPath = Path.Combine(new String[] {fileName, "..", "..", "..", "Examples"}); + + // Verify examples are present + if (Directory.Exists(expectedExamplesPath)) + { + DirectoryInfo examplesFolder = new DirectoryInfo(expectedExamplesPath); + FileInfo[] exampleFiles = examplesFolder.GetFiles(examplesQuery); + if (exampleFiles.Length != 0) + { + examplesPresent = true; + } + } + + // Return error if no examples present + if (!examplesPresent) + { + yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.DscExamplesPresentNoExamplesError, resourceName), + ast.Extent, GetName(), DiagnosticSeverity.Information, fileName); + } + } + + /// + /// AnalyzeDSCClass: Analyzes given DSC class + /// + /// + /// + /// + public IEnumerable AnalyzeDSCClass(Ast ast, string fileName) + { + String resourceName = null; + + IEnumerable dscClasses = ast.FindAll(item => + item is TypeDefinitionAst + && ((item as TypeDefinitionAst).IsClass) + && (item as TypeDefinitionAst).Attributes.Any(attr => String.Equals("DSCResource", attr.TypeName.FullName, StringComparison.OrdinalIgnoreCase)), true); + + foreach (TypeDefinitionAst dscClass in dscClasses) + { + resourceName = dscClass.Name; + + String examplesQuery = String.Format("*{0}*", resourceName); + Boolean examplesPresent = false; + String expectedExamplesPath = Path.Combine(new String[] {fileName, "..", "Examples"}); + + // Verify examples are present + if (Directory.Exists(expectedExamplesPath)) + { + DirectoryInfo examplesFolder = new DirectoryInfo(expectedExamplesPath); + FileInfo[] exampleFiles = examplesFolder.GetFiles(examplesQuery); + if (exampleFiles.Length != 0) + { + examplesPresent = true; + } + } + + // Return error if no examples present + if (!examplesPresent) + { + yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.DscExamplesPresentNoExamplesError, resourceName), + dscClass.Extent, GetName(), DiagnosticSeverity.Information, fileName); + } + } + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.DscExamplesPresent); + } + + /// + /// GetCommonName: Retrieves the Common name of this rule. + /// + /// The common name of this rule + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DscExamplesPresentCommonName); + } + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DscExamplesPresentDescription); + } + + /// + /// GetSourceType: Retrieves the type of the rule: builtin, managed or module. + /// + public SourceType GetSourceType() + { + return SourceType.Builtin; + } + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning or information. + /// + /// + public RuleSeverity GetSeverity() + { + return RuleSeverity.Information; + } + + /// + /// GetSourceName: Retrieves the module/assembly name the rule is from. + /// + public string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DSCSourceName); + } + } + +} \ No newline at end of file diff --git a/Rules/DscTestsPresent.cs b/Rules/DscTestsPresent.cs new file mode 100644 index 000000000..15fbbda23 --- /dev/null +++ b/Rules/DscTestsPresent.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Microsoft Corporation. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.Powershell.ScriptAnalyzer.Generic; +using System.ComponentModel.Composition; +using System.Globalization; +using System.IO; +using System.Management.Automation; + +namespace Microsoft.Windows.Powershell.ScriptAnalyzer.BuiltinRules +{ + /// + /// DscTestsPresent: Checks that DSC tests for given resource are present. + /// Rule expects directory Tests to be present: + /// For non-class based resources it should exist at the same folder level as DSCResources folder. + /// For class based resources it should be present at the same folder level as resource psm1 file. + /// Tests folder should contain test script for given resource - file name should contain resource's name. + /// + [Export(typeof(IDSCResourceRule))] + public class DscTestsPresent : IDSCResourceRule + { + /// + /// AnalyzeDSCResource: Analyzes given DSC Resource + /// + /// The script's ast + /// The name of the script file being analyzed + /// The results of the analysis + public IEnumerable AnalyzeDSCResource(Ast ast, string fileName) + { + String fileNameOnly = Path.GetFileName(fileName); + String resourceName = Path.GetFileNameWithoutExtension(fileNameOnly); + String testsQuery = String.Format("*{0}*", resourceName); + Boolean testsPresent = false; + String expectedTestsPath = Path.Combine(new String[] { fileName, "..", "..", "..", "Tests" }); + + // Verify tests are present + if (Directory.Exists(expectedTestsPath)) + { + DirectoryInfo testsFolder = new DirectoryInfo(expectedTestsPath); + FileInfo[] testFiles = testsFolder.GetFiles(testsQuery); + if (testFiles.Length != 0) + { + testsPresent = true; + } + } + + // Return error if no tests present + if (!testsPresent) + { + yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentNoTestsError, resourceName), + ast.Extent, GetName(), DiagnosticSeverity.Information, fileName); + } + } + + /// + /// AnalyzeDSCClass: Analyzes given DSC class + /// + /// + /// + /// + public IEnumerable AnalyzeDSCClass(Ast ast, string fileName) + { + String resourceName = null; + + IEnumerable dscClasses = ast.FindAll(item => + item is TypeDefinitionAst + && ((item as TypeDefinitionAst).IsClass) + && (item as TypeDefinitionAst).Attributes.Any(attr => String.Equals("DSCResource", attr.TypeName.FullName, StringComparison.OrdinalIgnoreCase)), true); + + foreach (TypeDefinitionAst dscClass in dscClasses) + { + resourceName = dscClass.Name; + + String testsQuery = String.Format("*{0}*", resourceName); + Boolean testsPresent = false; + String expectedTestsPath = Path.Combine(new String[] { fileName, "..", "Tests" }); + + // Verify tests are present + if (Directory.Exists(expectedTestsPath)) + { + DirectoryInfo testsFolder = new DirectoryInfo(expectedTestsPath); + FileInfo[] testFiles = testsFolder.GetFiles(testsQuery); + if (testFiles.Length != 0) + { + testsPresent = true; + } + } + + // Return error if no tests present + if (!testsPresent) + { + yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentNoTestsError, resourceName), + dscClass.Extent, GetName(), DiagnosticSeverity.Information, fileName); + } + } + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.DscTestsPresent); + } + + /// + /// GetCommonName: Retrieves the Common name of this rule. + /// + /// The common name of this rule + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentCommonName); + } + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentDescription); + } + + /// + /// GetSourceType: Retrieves the type of the rule: builtin, managed or module. + /// + public SourceType GetSourceType() + { + return SourceType.Builtin; + } + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning or information. + /// + /// + public RuleSeverity GetSeverity() + { + return RuleSeverity.Information; + } + + /// + /// GetSourceName: Retrieves the module/assembly name the rule is from. + /// + public string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DSCSourceName); + } + } + +} \ No newline at end of file diff --git a/Rules/ReturnCorrectTypesForDSCFunctions.cs b/Rules/ReturnCorrectTypesForDSCFunctions.cs index 2890660fb..90aeeeabc 100644 --- a/Rules/ReturnCorrectTypesForDSCFunctions.cs +++ b/Rules/ReturnCorrectTypesForDSCFunctions.cs @@ -210,7 +210,7 @@ public SourceType GetSourceType() } /// - /// GetSeverity: Retrieves the severity of the rule: error, warning of information. + /// GetSeverity: Retrieves the severity of the rule: error, warning or information. /// /// public RuleSeverity GetSeverity() diff --git a/Rules/ScriptAnalyzerBuiltinRules.csproj b/Rules/ScriptAnalyzerBuiltinRules.csproj index 7cc1c0446..7401ac269 100644 --- a/Rules/ScriptAnalyzerBuiltinRules.csproj +++ b/Rules/ScriptAnalyzerBuiltinRules.csproj @@ -68,6 +68,9 @@ + + + diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index 6c1285181..641440213 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -825,6 +825,42 @@ internal static string CommandNotFoundName { } } + /// + /// Looks up a localized string similar to DscExamplesPresent. + /// + internal static string DscExamplesPresent { + get { + return ResourceManager.GetString("DscExamplesPresent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DSC examples are present. + /// + internal static string DscExamplesPresentCommonName { + get { + return ResourceManager.GetString("DscExamplesPresentCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title.. + /// + internal static string DscExamplesPresentDescription { + get { + return ResourceManager.GetString("DscExamplesPresentDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No examples found for resource '{0}'. + /// + internal static string DscExamplesPresentNoExamplesError { + get { + return ResourceManager.GetString("DscExamplesPresentNoExamplesError", resourceCulture); + } + } + /// /// Looks up a localized string similar to PSDSC. /// @@ -834,6 +870,42 @@ internal static string DSCSourceName { } } + /// + /// Looks up a localized string similar to DscTestsPresent. + /// + internal static string DscTestsPresent { + get { + return ResourceManager.GetString("DscTestsPresent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dsc tests are present. + /// + internal static string DscTestsPresentCommonName { + get { + return ResourceManager.GetString("DscTestsPresentCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name.. + /// + internal static string DscTestsPresentDescription { + get { + return ResourceManager.GetString("DscTestsPresentDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No tests found for resource '{0}'. + /// + internal static string DscTestsPresentNoTestsError { + get { + return ResourceManager.GetString("DscTestsPresentNoTestsError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Module Manifest Fields. /// @@ -1545,6 +1617,42 @@ internal static string UseIdenticalParametersDSCName { } } + /// + /// Looks up a localized string similar to Use OutputType Correctly. + /// + internal static string UseOutputTypeCorrectlyCommonName { + get { + return ResourceManager.GetString("UseOutputTypeCorrectlyCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The return types of a cmdlet should be declared using the OutputType attribute.. + /// + internal static string UseOutputTypeCorrectlyDescription { + get { + return ResourceManager.GetString("UseOutputTypeCorrectlyDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute.. + /// + internal static string UseOutputTypeCorrectlyError { + get { + return ResourceManager.GetString("UseOutputTypeCorrectlyError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UseOutputTypeCorrectly. + /// + internal static string UseOutputTypeCorrectlyName { + get { + return ResourceManager.GetString("UseOutputTypeCorrectlyName", resourceCulture); + } + } + /// /// Looks up a localized string similar to PSCredential. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 607777527..b26e4d6fa 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -678,4 +678,40 @@ AvoidUsingWMICmdlet + + Use OutputType Correctly + + + The return types of a cmdlet should be declared using the OutputType attribute. + + + The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute. + + + UseOutputTypeCorrectly + + + DscTestsPresent + + + Dsc tests are present + + + Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name. + + + No tests found for resource '{0}' + + + DscExamplesPresent + + + DSC examples are present + + + Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title. + + + No examples found for resource '{0}' + \ No newline at end of file diff --git a/Rules/UseOutputTypeCorrectly.cs b/Rules/UseOutputTypeCorrectly.cs new file mode 100644 index 000000000..f79ccea15 --- /dev/null +++ b/Rules/UseOutputTypeCorrectly.cs @@ -0,0 +1,179 @@ +// +// Copyright (c) Microsoft Corporation. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.Powershell.ScriptAnalyzer.Generic; +using System.ComponentModel.Composition; +using System.Globalization; +using System.Management.Automation; + +namespace Microsoft.Windows.Powershell.ScriptAnalyzer.BuiltinRules +{ + /// + /// ProvideCommentHelp: Checks that objects return in a cmdlet have their types declared in OutputType Attribute + /// + [Export(typeof(IScriptRule))] + public class UseOutputTypeCorrectly : SkipTypeDefinition, IScriptRule + { + private IEnumerable _classes; + + /// + /// AnalyzeScript: Checks that objects return in a cmdlet have their types declared in OutputType Attribute + /// + /// The script's ast + /// The name of the script + /// A List of diagnostic results of this rule + public IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); + + DiagnosticRecords.Clear(); + this.fileName = fileName; + + _classes = ast.FindAll(item => item is TypeDefinitionAst && ((item as TypeDefinitionAst).IsClass), true).Cast(); + + ast.Visit(this); + + return DiagnosticRecords; + } + + /// + /// Visit function and checks that it is a cmdlet. If yes, then checks that any object returns must have a type declared + /// in the output type (the only exception is if the type is object) + /// + /// + /// + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst funcAst) + { + if (funcAst == null || funcAst.Body == null || funcAst.Body.ParamBlock == null + || funcAst.Body.ParamBlock.Attributes == null || funcAst.Body.ParamBlock.Attributes.Count == 0 + || !funcAst.Body.ParamBlock.Attributes.Any(attr => attr.TypeName.GetReflectionType() == typeof(CmdletBindingAttribute))) + { + return AstVisitAction.Continue; + } + + HashSet outputTypes = new HashSet(); + + foreach (AttributeAst attrAst in funcAst.Body.ParamBlock.Attributes) + { + if (attrAst.TypeName != null && attrAst.TypeName.GetReflectionType() == typeof(OutputTypeAttribute) + && attrAst.PositionalArguments != null) + { + foreach (ExpressionAst expAst in attrAst.PositionalArguments) + { + if (expAst is StringConstantExpressionAst) + { + Type type = Type.GetType((expAst as StringConstantExpressionAst).Value); + if (type != null) + { + outputTypes.Add(type.FullName); + } + } + else + { + TypeExpressionAst typeAst = expAst as TypeExpressionAst; + if (typeAst != null && typeAst.TypeName != null) + { + if (typeAst.TypeName.GetReflectionType() != null) + { + outputTypes.Add(typeAst.TypeName.GetReflectionType().FullName); + } + else + { + outputTypes.Add(typeAst.TypeName.FullName); + } + } + } + } + } + } + + List> returnTypes = FindPipelineOutput.OutputTypes(funcAst, _classes); + + foreach (Tuple returnType in returnTypes) + { + string typeName = returnType.Item1; + + if (String.IsNullOrEmpty(typeName) + || String.Equals(typeof(Unreached).FullName, typeName, StringComparison.OrdinalIgnoreCase) + || String.Equals(typeof(Undetermined).FullName, typeName, StringComparison.OrdinalIgnoreCase) + || String.Equals(typeof(object).FullName, typeName, StringComparison.OrdinalIgnoreCase) + || outputTypes.Contains(typeName, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + else + { + DiagnosticRecords.Add(new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.UseOutputTypeCorrectlyError, + funcAst.Name, typeName), returnType.Item2.Extent, GetName(), DiagnosticSeverity.Information, fileName)); + } + } + + return AstVisitAction.Continue; + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseOutputTypeCorrectlyName); + } + + /// + /// GetCommonName: Retrieves the common name of this rule. + /// + /// The common name of this rule + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseOutputTypeCorrectlyCommonName); + } + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseOutputTypeCorrectlyDescription); + } + + /// + /// Method: Retrieves the type of the rule: builtin, managed or module. + /// + public SourceType GetSourceType() + { + return SourceType.Builtin; + } + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning of information. + /// + /// + public RuleSeverity GetSeverity() + { + return RuleSeverity.Information; + } + + /// + /// Method: Retrieves the module/assembly name the rule is from. + /// + public string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + } +} diff --git a/Tests/Rules/BadCmdlet.ps1 b/Tests/Rules/BadCmdlet.ps1 index 546dc31a8..2958a0778 100644 --- a/Tests/Rules/BadCmdlet.ps1 +++ b/Tests/Rules/BadCmdlet.ps1 @@ -7,6 +7,8 @@ ConfirmImpact='Medium')] [Alias()] [OutputType([String])] + [OutputType("System.Int32", ParameterSetName="ID")] + Param ( # Param1 help description @@ -46,6 +48,14 @@ } Process { + $a = 4.5 + + if ($true) + { + $a + } + + return @{"hash"="true"} } End { @@ -53,6 +63,7 @@ } # Provide comment help should not be raised here because this is not exported +#Output type rule should also not be raised here as this is not a cmdlet function NoComment { Write-Verbose "No Comment" diff --git a/Tests/Rules/DscExamplesPresent.tests.ps1 b/Tests/Rules/DscExamplesPresent.tests.ps1 new file mode 100644 index 000000000..e27459a9a --- /dev/null +++ b/Tests/Rules/DscExamplesPresent.tests.ps1 @@ -0,0 +1,70 @@ +Import-Module -Verbose PSScriptAnalyzer + +$currentPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$ruleName = "PSDSCDscExamplesPresent" + +Describe "DscExamplesPresent rule in class based resource" { + + $examplesPath = "$currentPath\DSCResources\MyDscResource\Examples" + $classResourcePath = "$currentPath\DSCResources\MyDscResource\MyDscResource.psm1" + + Context "When examples absent" { + + $violations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $classResourcePath | Where-Object {$_.RuleName -eq $ruleName} + $violationMessage = "No examples found for resource 'FileResource'" + + It "has 1 missing examples violation" { + $violations.Count | Should Be 1 + } + + It "has the correct description message" { + $violations[0].Message | Should Match $violationMessage + } + } + + Context "When examples present" { + New-Item -Path $examplesPath -ItemType Directory + New-Item -Path "$examplesPath\FileResource_Example.psm1" -ItemType File + + $noViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $classResourcePath | Where-Object {$_.RuleName -eq $ruleName} + + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + + Remove-Item -Path $examplesPath -Recurse -Force + } +} + +Describe "DscExamplesPresent rule in regular (non-class) based resource" { + + $examplesPath = "$currentPath\Examples" + $resourcePath = "$currentPath\DSCResources\MSFT_WaitForAll\MSFT_WaitForAll.psm1" + + Context "When examples absent" { + + $violations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $resourcePath | Where-Object {$_.RuleName -eq $ruleName} + $violationMessage = "No examples found for resource 'MSFT_WaitForAll'" + + It "has 1 missing examples violation" { + $violations.Count | Should Be 1 + } + + It "has the correct description message" { + $violations[0].Message | Should Match $violationMessage + } + } + + Context "When examples present" { + New-Item -Path $examplesPath -ItemType Directory + New-Item -Path "$examplesPath\MSFT_WaitForAll_Example.psm1" -ItemType File + + $noViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $resourcePath | Where-Object {$_.RuleName -eq $ruleName} + + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + + Remove-Item -Path $examplesPath -Recurse -Force + } +} \ No newline at end of file diff --git a/Tests/Rules/DscTestsPresent.tests.ps1 b/Tests/Rules/DscTestsPresent.tests.ps1 new file mode 100644 index 000000000..03fbeaa66 --- /dev/null +++ b/Tests/Rules/DscTestsPresent.tests.ps1 @@ -0,0 +1,70 @@ +Import-Module -Verbose PSScriptAnalyzer + +$currentPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$ruleName = "PSDSCDscTestsPresent" + +Describe "DscTestsPresent rule in class based resource" { + + $testsPath = "$currentPath\DSCResources\MyDscResource\Tests" + $classResourcePath = "$currentPath\DSCResources\MyDscResource\MyDscResource.psm1" + + Context "When tests absent" { + + $violations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $classResourcePath | Where-Object {$_.RuleName -eq $ruleName} + $violationMessage = "No tests found for resource 'FileResource'" + + It "has 1 missing test violation" { + $violations.Count | Should Be 1 + } + + It "has the correct description message" { + $violations[0].Message | Should Be $violationMessage + } + } + + Context "When tests present" { + New-Item -Path $testsPath -ItemType Directory + New-Item -Path "$testsPath\FileResource_Test.psm1" -ItemType File + + $noViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $classResourcePath | Where-Object {$_.RuleName -eq $ruleName} + + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + + Remove-Item -Path $testsPath -Recurse -Force + } +} + +Describe "DscTestsPresent rule in regular (non-class) based resource" { + + $testsPath = "$currentPath\Tests" + $resourcePath = "$currentPath\DSCResources\MSFT_WaitForAll\MSFT_WaitForAll.psm1" + + Context "When tests absent" { + + $violations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $resourcePath | Where-Object {$_.RuleName -eq $ruleName} + $violationMessage = "No tests found for resource 'MSFT_WaitForAll'" + + It "has 1 missing tests violation" { + $violations.Count | Should Be 1 + } + + It "has the correct description message" { + $violations[0].Message | Should Be $violationMessage + } + } + + Context "When tests present" { + New-Item -Path $testsPath -ItemType Directory + New-Item -Path "$testsPath\MSFT_WaitForAll_Test.psm1" -ItemType File + + $noViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $resourcePath | Where-Object {$_.RuleName -eq $ruleName} + + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + + Remove-Item -Path $testsPath -Recurse -Force + } +} \ No newline at end of file diff --git a/Tests/Rules/GoodCmdlet.ps1 b/Tests/Rules/GoodCmdlet.ps1 index 7c1dfa240..73ea8399c 100644 --- a/Tests/Rules/GoodCmdlet.ps1 +++ b/Tests/Rules/GoodCmdlet.ps1 @@ -28,7 +28,9 @@ function Get-File HelpUri = 'http://www.microsoft.com/', ConfirmImpact='Medium')] [Alias()] - [OutputType([String])] + [OutputType([String], [System.Double], [Hashtable])] + [OutputType("System.Int32", ParameterSetName="ID")] + Param ( # Param1 help description @@ -75,6 +77,15 @@ function Get-File Write-Verbose "Write Verbose" Get-Process } + + $a = 4.5 + + if ($true) + { + $a + } + + return @{"hash"="true"} } End { diff --git a/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 b/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 new file mode 100644 index 000000000..666d281d0 --- /dev/null +++ b/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 @@ -0,0 +1,29 @@ +Import-Module PSScriptAnalyzer +$violationMessage = "The cmdlet 'Verb-Files' returns an object of type 'System.Double' but this type is not declared in the OutputType attribute." +$violationName = "PSUseOutputTypeCorrectly" +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$violations = Invoke-ScriptAnalyzer $directory\BadCmdlet.ps1 | Where-Object {$_.RuleName -eq $violationName} +$dscViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $directory\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} +$noViolations = Invoke-ScriptAnalyzer $directory\GoodCmdlet.ps1 | Where-Object {$_.RuleName -eq $violationName} + +Describe "UseOutputTypeCorrectly" { + Context "When there are violations" { + It "has 2 Use OutputType Correctly violations" { + $violations.Count | Should Be 2 + } + + It "has the correct description message" { + $violations[0].Message | Should Match $violationMessage + } + + It "Does not count violation in DSC class" { + $dscViolations.Count | Should Be 0 + } + } + + Context "When there are no violations" { + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + } +} \ No newline at end of file