This commit is contained in:
Bernie White 2020-09-03 23:41:36 +10:00 коммит произвёл GitHub
Родитель 76b184999e
Коммит 2ffa9a0ade
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 316 добавлений и 49 удалений

Просмотреть файл

@ -3,52 +3,20 @@
# Synopsis: Check for recommended community files # Synopsis: Check for recommended community files
Rule 'OpenSource.Community' -Type 'System.IO.DirectoryInfo', 'PSRule.Data.RepositoryInfo' { Rule 'OpenSource.Community' -Type 'System.IO.DirectoryInfo', 'PSRule.Data.RepositoryInfo' {
$requiredFiles = @( $Assert.FilePath($TargetObject, 'FullName', @('CHANGELOG.md'));
'CHANGELOG.md' $Assert.FilePath($TargetObject, 'FullName', @('LICENSE', 'LICENSE.txt'));
'LICENSE.txt' $Assert.FilePath($TargetObject, 'FullName', @('CODE_OF_CONDUCT.md'));
'CODE_OF_CONDUCT.md' $Assert.FilePath($TargetObject, 'FullName', @('CONTRIBUTING.md'));
'CONTRIBUTING.md' $Assert.FilePath($TargetObject, 'FullName', @('SECURITY.md'));
'SECURITY.md' $Assert.FilePath($TargetObject, 'FullName', @('README.md'));
'README.md' $Assert.FilePath($TargetObject, 'FullName', @('.github/CODEOWNERS'));
'.github/CODEOWNERS' $Assert.FilePath($TargetObject, 'FullName', @('.github/PULL_REQUEST_TEMPLATE.md'));
'.github/PULL_REQUEST_TEMPLATE.md'
)
Test-Path -Path $TargetObject.FullName;
for ($i = 0; $i -lt $requiredFiles.Length; $i++) {
$filePath = Join-Path -Path $TargetObject.FullName -ChildPath $requiredFiles[$i];
$Assert.Create((Test-Path -Path $filePath -PathType Leaf), "$($requiredFiles[$i]) does not exist");
}
} }
# Synopsis: Check for license in code files # Synopsis: Check for license in code files
Rule 'OpenSource.License' -Type 'System.IO.FileInfo', 'PSRule.Data.InputFileInfo', '.cs', '.ps1', '.psd1', '.psm1' -If { $TargetObject.Extension -in '.cs', '.ps1', '.psd1', '.psm1' } { Rule 'OpenSource.License' -Type '.cs', '.ps1', '.psd1', '.psm1' {
$commentPrefix = "`# "; $Assert.FileHeader($TargetObject, 'FullName', @(
if ($TargetObject.Extension -eq '.cs') { 'Copyright (c) Microsoft Corporation.'
$commentPrefix = '// ' 'Licensed under the MIT License.'
} ));
$header = GetLicenseHeader -CommentPrefix $commentPrefix;
$content = Get-Content -Path $TargetObject.FullName -Raw;
$content.StartsWith($header);
}
function global:GetLicenseHeader {
[CmdletBinding()]
[OutputType([String])]
param (
[Parameter(Mandatory = $True)]
[String]$CommentPrefix
)
process {
$text = @(
'Copyright (c) Microsoft Corporation.'
'Licensed under the MIT License.'
)
$builder = [System.Text.StringBuilder]::new();
foreach ($line in $text) {
$Null = $builder.Append($CommentPrefix);
$Null = $builder.Append($line);
$Null = $builder.Append([System.Environment]::NewLine);
}
return $builder.ToString();
}
} }

Просмотреть файл

@ -6,6 +6,10 @@
## Unreleased ## Unreleased
- Engine features:
- Added file assertion helpers `FileHeader`, and `FilePath`. [#534](https://github.com/microsoft/PSRule/issues/534)
- `FileHeader` checks for a comment header in the file.
- `FilePath` checks that a file path, optionally with suffixes exist.
- Bug fixes: - Bug fixes:
- Fixed out of bounds exception when empty markdown documentation is used. [#516](https://github.com/microsoft/PSRule/issues/516) - Fixed out of bounds exception when empty markdown documentation is used. [#516](https://github.com/microsoft/PSRule/issues/516)

Просмотреть файл

@ -235,6 +235,8 @@ The following conceptual topics exist in the `PSRule` module:
- [Assert](docs/concepts/PSRule/en-US/about_PSRule_Assert.md) - [Assert](docs/concepts/PSRule/en-US/about_PSRule_Assert.md)
- [Contains](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#contains) - [Contains](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#contains)
- [EndsWith](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#endswith) - [EndsWith](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#endswith)
- [FileHeader](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#fileheader)
- [FilePath](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#filepath)
- [Greater](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#greater) - [Greater](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#greater)
- [GreaterOrEqual](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#greaterorequal) - [GreaterOrEqual](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#greaterorequal)
- [HasDefaultValue](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#hasdefaultvalue) - [HasDefaultValue](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#hasdefaultvalue)

Просмотреть файл

@ -17,6 +17,8 @@ The following built-in assertion methods are provided:
- [Contains](#contains) - The field value must contain at least one of the strings. - [Contains](#contains) - The field value must contain at least one of the strings.
- [EndsWith](#endswith) - The field value must match at least one suffix. - [EndsWith](#endswith) - The field value must match at least one suffix.
- [FileHeader](#fileheader) - The file must contain a comment header.
- [FilePath](#filepath) - The file path must exist.
- [Greater](#greater) - The field value must be greater. - [Greater](#greater) - The field value must be greater.
- [GreaterOrEqual](#greaterorequal) - The field value must be greater or equal to. - [GreaterOrEqual](#greaterorequal) - The field value must be greater or equal to.
- [HasDefaultValue](#hasdefaultvalue) - The object should not have the field or the field value is set to the default value. - [HasDefaultValue](#hasdefaultvalue) - The object should not have the field or the field value is set to the default value.
@ -129,6 +131,8 @@ The following parameters are accepted:
- `suffix` - One or more suffixes to compare the field value with. Only one suffix must match. - `suffix` - One or more suffixes to compare the field value with. Only one suffix must match.
- `caseSensitive` (optional) - Use a case sensitive compare of the field value. Case is ignored by default. - `caseSensitive` (optional) - Use a case sensitive compare of the field value. Case is ignored by default.
Reasons include:
- _The parameter 'inputObject' is null._ - _The parameter 'inputObject' is null._
- _The parameter 'field' is null or empty._ - _The parameter 'field' is null or empty._
- _The field '{0}' does not exist._ - _The field '{0}' does not exist._
@ -144,6 +148,83 @@ Rule 'EndsWith' {
} }
``` ```
### FileHeader
The `FileHeader` assertion method checks a file for a comment header.
When comparing the file header, the format of line comments are automatically detected by file extension.
Single line comments are supported. Multi-line comments are not supported.
The following parameters are accepted:
- `inputObject` - The object being checked for the specified field.
- `field` - The name of the field containing a valid file path.
- `header` - One or more lines of a header to compare with file contents.
- `prefix` (optional) - An optional comment prefix for each line.
By default a comment prefix will automatically detected based on file extension.
When set, detection by file extension is skipped.
Prefix detection for line comments is supported with the following file extensions:
- `.cs`, `.ts`, `.js`, `.fs`, `.go`, `.php`, `.cpp`, `.h` - Use a prefix of (`// `).
- `.ps1`, `.psd1`, `.psm1`, `.yaml`, `.yml`, `.r`, `.py`, `.sh`, `.tf`, `.tfvars`, `.gitignore`, `.pl` - Use a prefix of (`# `).
- `.sql`, `.lau` - Use a prefix of (`-- `).
Reasons include:
- _The parameter 'inputObject' is null._
- _The parameter 'field' is null or empty._
- _The field '{0}' does not exist._
- _The field value '{0}' is not a string._
- _The file '{0}' does not exist._
- _The header was not set._
Examples:
```powershell
Rule 'FileHeader' {
$Assert.FileHeader($TargetObject, 'FullName', @(
'Copyright (c) Microsoft Corporation.'
'Licensed under the MIT License.'
));
}
```
### FilePath
The `FilePath` assertion method checks the file exists.
Checks use OS case-sensitivity rules.
The following parameters are accepted:
- `inputObject` - The object being checked for the specified field.
- `field` - The name of the field containing a file path.
- `suffix` (optional) - Additional file path suffixes to append.
When specified each suffix is combined with the file path.
Only one full file path must be a valid file for the assertion method to pass.
Reasons include:
- _The parameter 'inputObject' is null._
- _The parameter 'field' is null or empty._
- _The field '{0}' does not exist._
- _The field value '{0}' is not a string._
- _The file '{0}' does not exist._
Examples:
```powershell
Rule 'FilePath' {
$Assert.FilePath($TargetObject, 'FullName', @('CHANGELOG.md'));
$Assert.FilePath($TargetObject, 'FullName', @('LICENSE', 'LICENSE.txt'));
$Assert.FilePath($TargetObject, 'FullName', @('CODE_OF_CONDUCT.md'));
$Assert.FilePath($TargetObject, 'FullName', @('CONTRIBUTING.md'));
$Assert.FilePath($TargetObject, 'FullName', @('SECURITY.md'));
$Assert.FilePath($TargetObject, 'FullName', @('README.md'));
$Assert.FilePath($TargetObject, 'FullName', @('.github/CODEOWNERS'));
$Assert.FilePath($TargetObject, 'FullName', @('.github/PULL_REQUEST_TEMPLATE.md'));
}
```
### Greater ### Greater
The `Greater` assertion method checks the field value is greater than the specified value. The `Greater` assertion method checks the field value is greater than the specified value.

18
src/PSRule/Resources/ReasonStrings.Designer.cs сгенерированный
Просмотреть файл

@ -105,6 +105,24 @@ namespace PSRule.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to The header was not set..
/// </summary>
internal static string FileHeader {
get {
return ResourceManager.GetString("FileHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The file &apos;{0}&apos; does not exist..
/// </summary>
internal static string FilePath {
get {
return ResourceManager.GetString("FilePath", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The value &apos;{0}&apos; was not &gt; &apos;{1}&apos;.. /// Looks up a localized string similar to The value &apos;{0}&apos; was not &gt; &apos;{1}&apos;..
/// </summary> /// </summary>

Просмотреть файл

@ -134,6 +134,12 @@
<value>The field(s) existed: {0}</value> <value>The field(s) existed: {0}</value>
<comment>Included when any of the fields exist and the -Not switch is used.</comment> <comment>Included when any of the fields exist and the -Not switch is used.</comment>
</data> </data>
<data name="FileHeader" xml:space="preserve">
<value>The header was not set.</value>
</data>
<data name="FilePath" xml:space="preserve">
<value>The file '{0}' does not exist.</value>
</data>
<data name="Greater" xml:space="preserve"> <data name="Greater" xml:space="preserve">
<value>The value '{0}' was not &gt; '{1}'.</value> <value>The value '{0}' was not &gt; '{1}'.</value>
</data> </data>

Просмотреть файл

@ -421,6 +421,73 @@ namespace PSRule.Runtime
return Fail(ReasonStrings.NotMatchPattern, value, pattern); return Fail(ReasonStrings.NotMatchPattern, value, pattern);
} }
public AssertResult FilePath(PSObject inputObject, string field, string[] suffix = null)
{
// Guard parameters
if (GuardNullParam(inputObject, nameof(inputObject), out AssertResult result) ||
GuardNullOrEmptyParam(field, nameof(field), out result) ||
GuardField(inputObject, field, false, out object fieldValue, out result) ||
GuardString(fieldValue, out string value, out result))
return result;
if (suffix == null || suffix.Length == 0)
{
if (!TryFilePath(value, out _))
return Fail(ReasonStrings.FilePath, value);
return Pass();
}
var reason = Fail();
for (var i = 0; i < suffix.Length; i++)
{
if (!TryFilePath(Path.Combine(value, suffix[i]), out _))
reason.AddReason(ReasonStrings.FilePath, suffix[i]);
else
return Pass();
}
return reason;
}
public AssertResult FileHeader(PSObject inputObject, string field, string[] header, string prefix = null)
{
// Guard parameters
if (GuardNullParam(inputObject, nameof(inputObject), out AssertResult result) ||
GuardNullOrEmptyParam(field, nameof(field), out result) ||
GuardField(inputObject, field, false, out object fieldValue, out result) ||
GuardString(fieldValue, out string value, out result))
return result;
// File does not exist
if (!TryFilePath(value, out _))
return Fail(ReasonStrings.FilePath, value);
// No header
if (header == null || header.Length == 0)
return Pass();
if (string.IsNullOrEmpty(prefix))
prefix = DetectLinePrefix(Path.GetExtension(value));
var lineNo = 0;
foreach (var content in File.ReadLines(value))
{
if (lineNo >= header.Length)
break;
if (content != string.Concat(prefix, header[lineNo]))
return Fail(ReasonStrings.FileHeader);
lineNo++;
}
// Catch file has less lines than header
if (lineNo < header.Length)
return Fail(ReasonStrings.FileHeader);
return Pass();
}
#region Helper methods #region Helper methods
private static bool IsEmpty(object fieldValue) private static bool IsEmpty(object fieldValue)
@ -472,7 +539,7 @@ namespace PSRule.Runtime
private static bool TryString(object obj, out string value) private static bool TryString(object obj, out string value)
{ {
value = null; value = null;
if (obj is string svalue) if (GetBaseObject(obj) is string svalue)
{ {
value = svalue; value = svalue;
return true; return true;
@ -656,10 +723,10 @@ namespace PSRule.Runtime
return false; return false;
} }
private static bool TryFilePath(string uri, out string path) private static bool TryFilePath(string path, out string rootedPath)
{ {
path = PSRuleOption.GetRootedPath(uri); rootedPath = PSRuleOption.GetRootedPath(path);
return File.Exists(path); return File.Exists(rootedPath);
} }
private static string FormatArray(string[] values) private static string FormatArray(string[] values)
@ -697,6 +764,46 @@ namespace PSRule.Runtime
PipelineContext.CurrentThread.ExpressionCache[string.Concat(prefix, key)] = value; PipelineContext.CurrentThread.ExpressionCache[string.Concat(prefix, key)] = value;
} }
/// <summary>
/// Determine line comment prefix by file extension
/// </summary>
private static string DetectLinePrefix(string extension)
{
switch (extension)
{
case ".cs":
case ".ts":
case ".js":
case ".fs":
case ".go":
case ".php":
case ".cpp":
case ".h":
return "// ";
case ".ps1":
case ".psd1":
case ".psm1":
case ".yaml":
case ".yml":
case ".r":
case ".py":
case ".sh":
case ".tf":
case ".tfvars":
case ".gitignore":
case ".pl":
return "# ";
case ".sql":
case ".lua":
return "-- ";
default:
return string.Empty;
}
}
#endregion Helper methods #endregion Helper methods
} }
} }

Просмотреть файл

@ -2,6 +2,8 @@
// Licensed under the MIT License. // Licensed under the MIT License.
using PSRule.Pipeline; using PSRule.Pipeline;
using System;
using System.IO;
using System.Management.Automation; using System.Management.Automation;
using Xunit; using Xunit;
@ -493,6 +495,30 @@ namespace PSRule
Assert.True(assert.NotMatch(value, "notValue", "\\w*2").Result); Assert.True(assert.NotMatch(value, "notValue", "\\w*2").Result);
} }
[Fact]
public void FileHeader()
{
SetContext();
var assert = GetAssertionHelper();
var value = GetObject((name: "FullName", value: GetSourcePath("FromFile.Rule.ps1")));
Assert.True(assert.FileHeader(value, "FullName", new string[] { "Copyright (c) Microsoft Corporation.", "Licensed under the MIT License." }).Result);
value = GetObject((name: "FullName", value: GetSourcePath("Baseline.Rule.yaml")));
Assert.False(assert.FileHeader(value, "FullName", new string[] { "Copyright (c) Microsoft Corporation.", "Licensed under the MIT License." }).Result);
}
[Fact]
public void FilePath()
{
SetContext();
var assert = GetAssertionHelper();
var value = GetObject((name: "FullName", value: GetSourcePath("Baseline.Rule.yaml")));
Assert.True(assert.FilePath(value, "FullName").Result);
value = GetObject((name: "FullName", value: GetSourcePath("README.zz")));
Assert.False(assert.FilePath(value, "FullName").Result);
}
#region Helper methods #region Helper methods
private static void SetContext() private static void SetContext()
@ -516,6 +542,11 @@ namespace PSRule
return new Runtime.Assert(); return new Runtime.Assert();
} }
private string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}
#endregion Helper methods #endregion Helper methods
} }
} }

Просмотреть файл

@ -52,6 +52,20 @@ Rule 'Assert.EndsWith' {
$Assert.EndsWith($TargetObject, 'Name', '1') $Assert.EndsWith($TargetObject, 'Name', '1')
} }
# Synopsis: Test for $Assert.FileHeader
Rule 'Assert.FileHeader' {
$Assert.FileHeader($TargetObject, 'Path', @(
'Copyright (c) Microsoft Corporation.'
'Licensed under the MIT License.'
))
}
# Synopsis: Test for $Assert.FilePath
Rule 'Assert.FilePath' {
$Assert.FilePath($TargetObject, 'Path')
$Assert.FilePath($TargetObject, 'ParentPath', @('PSRule.Assert.Tests.ps1'))
}
# Synopsis: Test for $Assert.Greater # Synopsis: Test for $Assert.Greater
Rule 'Assert.Greater' { Rule 'Assert.Greater' {
$Assert.Greater($TargetObject, 'CompareNumeric', 2) $Assert.Greater($TargetObject, 'CompareNumeric', 2)

Просмотреть файл

@ -49,6 +49,8 @@ Describe 'PSRule assertions' -Tag 'Assert' {
'Item3' 'Item3'
'Item4' 'Item4'
) )
Path = $PSCommandPath
ParentPath = $here
} }
[PSCustomObject]@{ [PSCustomObject]@{
'$schema' = "http://json-schema.org/draft-07/schema`#" '$schema' = "http://json-schema.org/draft-07/schema`#"
@ -75,6 +77,7 @@ Describe 'PSRule assertions' -Tag 'Assert' {
'item2' 'item2'
'item3' 'item3'
) )
ParentPath = (Join-Path -Path $here -ChildPath 'notapath')
} }
) )
@ -162,6 +165,39 @@ Describe 'PSRule assertions' -Tag 'Assert' {
$result[1].Reason | Should -BeLike "The field '*' does not end with '*'."; $result[1].Reason | Should -BeLike "The field '*' does not end with '*'.";
} }
It 'FileHeader' {
$result = @($testObject | Invoke-PSRule -Path $ruleFilePath -Name 'Assert.FileHeader');
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 2;
# Positive case
$result[0].IsSuccess() | Should -Be $True;
$result[0].TargetName | Should -Be 'TestObject1';
# Negative case
$result[1].IsSuccess() | Should -Be $False;
$result[1].TargetName | Should -Be 'TestObject2';
$result[1].Reason.Length | Should -Be 1;
$result[1].Reason | Should -Be "The field 'Path' does not exist.";
}
It 'FilePath' {
$result = @($testObject | Invoke-PSRule -Path $ruleFilePath -Name 'Assert.FilePath');
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 2;
# Positive case
$result[0].IsSuccess() | Should -Be $True;
$result[0].TargetName | Should -Be 'TestObject1';
# Negative case
$result[1].IsSuccess() | Should -Be $False;
$result[1].TargetName | Should -Be 'TestObject2';
$result[1].Reason.Length | Should -Be 2;
$result[1].Reason[0] | Should -Be "The field 'Path' does not exist.";
$result[1].Reason[1] | Should -BeLike "The file '*' does not exist.";
}
It 'Greater' { It 'Greater' {
$result = @($testObject | Invoke-PSRule -Path $ruleFilePath -Name 'Assert.Greater'); $result = @($testObject | Invoke-PSRule -Path $ruleFilePath -Name 'Assert.Greater');
$result | Should -Not -BeNullOrEmpty; $result | Should -Not -BeNullOrEmpty;