зеркало из https://github.com/microsoft/PSRule.git
Родитель
76b184999e
Коммит
2ffa9a0ade
|
@ -3,52 +3,20 @@
|
|||
|
||||
# Synopsis: Check for recommended community files
|
||||
Rule 'OpenSource.Community' -Type 'System.IO.DirectoryInfo', 'PSRule.Data.RepositoryInfo' {
|
||||
$requiredFiles = @(
|
||||
'CHANGELOG.md'
|
||||
'LICENSE.txt'
|
||||
'CODE_OF_CONDUCT.md'
|
||||
'CONTRIBUTING.md'
|
||||
'SECURITY.md'
|
||||
'README.md'
|
||||
'.github/CODEOWNERS'
|
||||
'.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");
|
||||
}
|
||||
$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'));
|
||||
}
|
||||
|
||||
# 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' } {
|
||||
$commentPrefix = "`# ";
|
||||
if ($TargetObject.Extension -eq '.cs') {
|
||||
$commentPrefix = '// '
|
||||
}
|
||||
$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();
|
||||
}
|
||||
Rule 'OpenSource.License' -Type '.cs', '.ps1', '.psd1', '.psm1' {
|
||||
$Assert.FileHeader($TargetObject, 'FullName', @(
|
||||
'Copyright (c) Microsoft Corporation.'
|
||||
'Licensed under the MIT License.'
|
||||
));
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
## 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:
|
||||
- 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)
|
||||
- [Contains](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#contains)
|
||||
- [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)
|
||||
- [GreaterOrEqual](docs/concepts/PSRule/en-US/about_PSRule_Assert.md#greaterorequal)
|
||||
- [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.
|
||||
- [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.
|
||||
- [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.
|
||||
|
@ -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.
|
||||
- `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 'field' is null or empty._
|
||||
- _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
|
||||
|
||||
The `Greater` assertion method checks the field value is greater than the specified value.
|
||||
|
|
|
@ -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 '{0}' does not exist..
|
||||
/// </summary>
|
||||
internal static string FilePath {
|
||||
get {
|
||||
return ResourceManager.GetString("FilePath", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The value '{0}' was not > '{1}'..
|
||||
/// </summary>
|
||||
|
|
|
@ -134,6 +134,12 @@
|
|||
<value>The field(s) existed: {0}</value>
|
||||
<comment>Included when any of the fields exist and the -Not switch is used.</comment>
|
||||
</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">
|
||||
<value>The value '{0}' was not > '{1}'.</value>
|
||||
</data>
|
||||
|
|
|
@ -421,6 +421,73 @@ namespace PSRule.Runtime
|
|||
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
|
||||
|
||||
private static bool IsEmpty(object fieldValue)
|
||||
|
@ -472,7 +539,7 @@ namespace PSRule.Runtime
|
|||
private static bool TryString(object obj, out string value)
|
||||
{
|
||||
value = null;
|
||||
if (obj is string svalue)
|
||||
if (GetBaseObject(obj) is string svalue)
|
||||
{
|
||||
value = svalue;
|
||||
return true;
|
||||
|
@ -656,10 +723,10 @@ namespace PSRule.Runtime
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFilePath(string uri, out string path)
|
||||
private static bool TryFilePath(string path, out string rootedPath)
|
||||
{
|
||||
path = PSRuleOption.GetRootedPath(uri);
|
||||
return File.Exists(path);
|
||||
rootedPath = PSRuleOption.GetRootedPath(path);
|
||||
return File.Exists(rootedPath);
|
||||
}
|
||||
|
||||
private static string FormatArray(string[] values)
|
||||
|
@ -697,6 +764,46 @@ namespace PSRule.Runtime
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Licensed under the MIT License.
|
||||
|
||||
using PSRule.Pipeline;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using Xunit;
|
||||
|
||||
|
@ -493,6 +495,30 @@ namespace PSRule
|
|||
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
|
||||
|
||||
private static void SetContext()
|
||||
|
@ -516,6 +542,11 @@ namespace PSRule
|
|||
return new Runtime.Assert();
|
||||
}
|
||||
|
||||
private string GetSourcePath(string fileName)
|
||||
{
|
||||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
|
||||
}
|
||||
|
||||
#endregion Helper methods
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,20 @@ Rule 'Assert.EndsWith' {
|
|||
$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
|
||||
Rule 'Assert.Greater' {
|
||||
$Assert.Greater($TargetObject, 'CompareNumeric', 2)
|
||||
|
|
|
@ -49,6 +49,8 @@ Describe 'PSRule assertions' -Tag 'Assert' {
|
|||
'Item3'
|
||||
'Item4'
|
||||
)
|
||||
Path = $PSCommandPath
|
||||
ParentPath = $here
|
||||
}
|
||||
[PSCustomObject]@{
|
||||
'$schema' = "http://json-schema.org/draft-07/schema`#"
|
||||
|
@ -75,6 +77,7 @@ Describe 'PSRule assertions' -Tag 'Assert' {
|
|||
'item2'
|
||||
'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 '*'.";
|
||||
}
|
||||
|
||||
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' {
|
||||
$result = @($testObject | Invoke-PSRule -Path $ruleFilePath -Name 'Assert.Greater');
|
||||
$result | Should -Not -BeNullOrEmpty;
|
||||
|
|
Загрузка…
Ссылка в новой задаче