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