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/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/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 eef54841b..a11c83100 100644 --- a/Rules/ScriptAnalyzerBuiltinRules.csproj +++ b/Rules/ScriptAnalyzerBuiltinRules.csproj @@ -68,6 +68,7 @@ + diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index 3f1eb4469..ef44c1fcd 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. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 1d548b83a..c57821d3e 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -690,4 +690,16 @@ UseOutputTypeCorrectly + + 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/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