diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/FileHeaderCodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/FileHeaderCodeFixProvider.cs
index 9d1877774..e6f13b6b4 100644
--- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/FileHeaderCodeFixProvider.cs
+++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/FileHeaderCodeFixProvider.cs
@@ -4,6 +4,7 @@
namespace StyleCop.Analyzers.DocumentationRules
{
using System;
+ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Text;
@@ -212,6 +213,9 @@ private static SyntaxNode ReplaceHeader(Document document, SyntaxNode root, Styl
var leadingSpaces = string.Empty;
string possibleLeadingSpaces = string.Empty;
+ // remove header decoration lines, they will be re-generated
+ trivia = RemoveHeaderDecorationLines(trivia, settings);
+
// Need to do this with index so we get the line endings correct.
for (int i = 0; i < trivia.Count; i++)
{
@@ -365,10 +369,20 @@ private static string WrapInXmlComment(string prefixWithLeadingSpaces, string co
string encodedCompanyName = new XAttribute("t", settings.DocumentationRules.CompanyName).ToString().Substring(2).Trim('"');
string encodedCopyrightText = new XText(copyrightText).ToString();
- return
+ string copyrightString =
$"{prefixWithLeadingSpaces} " + newLineText
+ encodedCopyrightText + newLineText
+ prefixWithLeadingSpaces + " ";
+
+ if (!string.IsNullOrEmpty(settings.DocumentationRules.HeaderDecoration))
+ {
+ return
+ $"{prefixWithLeadingSpaces} {settings.DocumentationRules.HeaderDecoration}" + newLineText
+ + copyrightString + newLineText
+ + $"{prefixWithLeadingSpaces} {settings.DocumentationRules.HeaderDecoration}";
+ }
+
+ return copyrightString;
}
private static string GetCopyrightText(string prefixWithLeadingSpaces, string copyrightText, string newLineText)
@@ -376,5 +390,35 @@ private static string GetCopyrightText(string prefixWithLeadingSpaces, string co
copyrightText = copyrightText.Replace("\r\n", "\n");
return string.Join(newLineText + prefixWithLeadingSpaces + " ", copyrightText.Split('\n')).Replace(prefixWithLeadingSpaces + " " + newLineText, prefixWithLeadingSpaces + newLineText);
}
+
+ private static SyntaxTriviaList RemoveHeaderDecorationLines(SyntaxTriviaList trivia, StyleCopSettings settings)
+ {
+ if (!string.IsNullOrEmpty(settings.DocumentationRules.HeaderDecoration))
+ {
+ var decorationRemovalList = new List();
+ for (int i = 0; i < trivia.Count; i++)
+ {
+ var triviaLine = trivia[i];
+ if (triviaLine.Kind() == SyntaxKind.SingleLineCommentTrivia && triviaLine.ToFullString().Contains(settings.DocumentationRules.HeaderDecoration))
+ {
+ decorationRemovalList.Add(i);
+
+ // also remove the line break
+ if (i + 1 < trivia.Count && trivia[i + 1].Kind() == SyntaxKind.EndOfLineTrivia)
+ {
+ decorationRemovalList.Add(i + 1);
+ }
+ }
+ }
+
+ // Remove decoration lines in reverse order.
+ for (int i = decorationRemovalList.Count - 1; i >= 0; i--)
+ {
+ trivia = trivia.RemoveAt(decorationRemovalList[i]);
+ }
+ }
+
+ return trivia;
+ }
}
}
diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1633UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1633UnitTests.cs
index 33a5b1629..c878fd5f5 100644
--- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1633UnitTests.cs
+++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1633UnitTests.cs
@@ -24,10 +24,24 @@ public class SA1633UnitTests : FileHeaderTestBase
}
}
}
+";
+
+ private const string DecoratedXmlMultiLineHeaderTestSettings = @"
+{
+ ""settings"": {
+ ""documentationRules"": {
+ ""companyName"": ""FooCorp"",
+ ""copyrightText"": "" Copyright (c) {companyName}. All rights reserved."",
+ ""headerDecoration"": ""-----------------------------------------------------------------------"",
+ }
+ }
+}
";
private bool useNoXmlSettings;
+ private bool useDecoratedXmlMultiLineHeaderTestSettings;
+
///
/// Verifies that the analyzer will report for
/// projects using XML headers (the default) when the file is completely missing a header.
@@ -167,6 +181,36 @@ namespace Foo
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
}
+ ///
+ /// Verifies that a file without a header, but with leading trivia will produce the correct diagnostic message.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task TestMissingFileHeaderWithDecorationAsync()
+ {
+ var testCode = @"namespace Foo
+{
+}
+";
+ var fixedCode = @"// -----------------------------------------------------------------------
+//
+// Copyright (c) FooCorp. All rights reserved.
+//
+// -----------------------------------------------------------------------
+
+namespace Foo
+{
+}
+";
+
+ this.useDecoratedXmlMultiLineHeaderTestSettings = true;
+
+ var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1633DescriptorMissing).WithLocation(1, 1);
+ await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
+ await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
+ await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
+ }
+
///
/// Verifies that a file header without XML structure will produce the correct diagnostic message.
///
@@ -308,6 +352,11 @@ protected override string GetSettings()
return NoXmlMultiLineHeaderTestSettings;
}
+ if (this.useDecoratedXmlMultiLineHeaderTestSettings)
+ {
+ return DecoratedXmlMultiLineHeaderTestSettings;
+ }
+
return base.GetSettings();
}
diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1638UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1638UnitTests.cs
index 5b56ef35a..5a183baeb 100644
--- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1638UnitTests.cs
+++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1638UnitTests.cs
@@ -14,6 +14,20 @@ namespace StyleCop.Analyzers.Test.DocumentationRules
///
public class SA1638UnitTests : FileHeaderTestBase
{
+ private const string DecoratedXmlMultiLineHeaderTestSettings = @"
+{
+ ""settings"": {
+ ""documentationRules"": {
+ ""companyName"": ""FooCorp"",
+ ""copyrightText"": "" Copyright (c) {companyName}. All rights reserved."",
+ ""headerDecoration"": ""-----------------------------------------------------------------------"",
+ }
+ }
+}
+";
+
+ private bool useDecoratedXmlMultiLineHeaderTestSettings;
+
///
/// Verifies that a file header with a mismatching file attribute in the copyright element will produce the expected diagnostic message.
///
@@ -44,9 +58,56 @@ namespace Bar
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
}
+ ///
+ /// Verifies that a file header with a mismatching file attribute in the copyright element will produce the expected diagnostic message.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task TestCopyrightElementWithMismatchingFileAttributeAndDecorationAsync()
+ {
+ var testCode = @"// -----------------------------------------------------------------------
+//
+// Copyright (c) FooCorp. All rights reserved.
+//
+// -----------------------------------------------------------------------
+
+namespace Bar
+{
+}
+";
+ var fixedCode = @"// -----------------------------------------------------------------------
+//
+// Copyright (c) FooCorp. All rights reserved.
+//
+// -----------------------------------------------------------------------
+
+namespace Bar
+{
+}
+";
+
+ this.useDecoratedXmlMultiLineHeaderTestSettings = true;
+
+ var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1638Descriptor).WithLocation(2, 4);
+ await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
+ await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
+ await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
+ }
+
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return new FileHeaderCodeFixProvider();
}
+
+ ///
+ protected override string GetSettings()
+ {
+ if (this.useDecoratedXmlMultiLineHeaderTestSettings)
+ {
+ return DecoratedXmlMultiLineHeaderTestSettings;
+ }
+
+ return base.GetSettings();
+ }
}
}
diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Settings/ObjectModel/DocumentationSettings.cs b/StyleCop.Analyzers/StyleCop.Analyzers/Settings/ObjectModel/DocumentationSettings.cs
index 23cdfe4b6..2433e1dde 100644
--- a/StyleCop.Analyzers/StyleCop.Analyzers/Settings/ObjectModel/DocumentationSettings.cs
+++ b/StyleCop.Analyzers/StyleCop.Analyzers/Settings/ObjectModel/DocumentationSettings.cs
@@ -32,6 +32,12 @@ internal class DocumentationSettings
[JsonProperty("copyrightText", DefaultValueHandling = DefaultValueHandling.Ignore)]
private string copyrightText;
+ ///
+ /// This is the backing field for the property.
+ ///
+ [JsonProperty("headerDecoration", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ private string headerDecoration;
+
///
/// This is the cache for the property.
///
@@ -94,6 +100,7 @@ protected internal DocumentationSettings()
this.companyName = DefaultCompanyName;
this.copyrightText = DefaultCopyrightText;
this.variables = ImmutableDictionary.Empty.ToBuilder();
+ this.headerDecoration = null;
this.xmlHeader = true;
this.documentExposedElements = true;
@@ -126,6 +133,14 @@ public string CopyrightText
}
}
+ public string HeaderDecoration
+ {
+ get
+ {
+ return this.headerDecoration;
+ }
+ }
+
public ImmutableDictionary Variables
{
get
diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json b/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json
index d2aa0fb63..1d2cb335c 100644
--- a/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json
+++ b/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json
@@ -194,6 +194,11 @@
"description": "Determines whether the file header should be wrapped in the StyleCop-standard XML structure.",
"default": true
},
+ "headerDecoration": {
+ "type": "string",
+ "description": "The text used as decoration for the copyright header comment.",
+ "default": null
+ }
"fileNamingConvention": {
"type": "string",
"description": "Specifies the preferred naming convention for files. The default value \"stylecop\" uses the naming convention defined by StyleCop Classic, while \"metadata\" uses a file naming convention that matches the metadata names of types.",
diff --git a/documentation/Configuration.md b/documentation/Configuration.md
index 6e1dad3d7..a5c8a85ee 100644
--- a/documentation/Configuration.md
+++ b/documentation/Configuration.md
@@ -324,6 +324,7 @@ The following properties are used to configure copyright headers in StyleCop Ana
| `copyrightText` | `"Copyright (c) {companyName}. All rights reserved."` | Specifies the default copyright text which should appear in copyright headers |
| `xmlHeader` | **true** | Specifies whether file headers should use standard StyleCop XML format, where the copyright notice is wrapped in a `` element |
| `variables` | n/a | Specifies replacement variables which can be referenced in the `copyrightText` value |
+| `headerDecoration` | n/a | This value can be set to add a decoration for the header comment so that the headers look similar to the ones generated by the StyleCop classic R# fix |
#### Configuring Copyright Text
@@ -373,6 +374,35 @@ When the `xmlHeader` property is explicitly set to **false**, StyleCop Analyzers
// {copyrightText}
```
+#### Configuring Copyright Text Header Decoration
+
+The `headerDecoration` property is a string which can contain a string that's used for decorating the generated header so that the headers look similar to the ones generated by the StyleCop classic R# fix.
+
+The default value for the `headerDecoration` property is empty, so no decoration will be added.
+> The header decoration is not checked, it's only used for fixing the header.
+
+```json
+{
+ "settings": {
+ "documentationRules": {
+ "companyName": "FooCorp",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved.",
+ "headerDecoration": "-----------------------------------------------------------------------"
+ }
+ }
+}
+```
+
+With the above configuration, the fix for a file **TypeName.cs** would look like the following header.
+
+```csharp
+// -----------------------------------------------------------------------
+//
+// Copyright (c) FooCorp. All rights reserved.
+//
+// -----------------------------------------------------------------------
+```
+
### Documentation Requirements
StyleCop Analyzers includes rules which require developers to document the majority of a code base by default. This requirement can easily overwhelm a team which did not use StyleCop for the entire development process. To help guide developers towards a properly documented code base, several properties are available in **stylecop.json** to progressively increase the documentation requirements.