Skip to content

Commit 0d21757

Browse files
committed
Merge pull request #2087 from ThomasZitzler/fileheaderfix
add file header decoration comments if configured
2 parents 088e573 + b63e425 commit 0d21757

6 files changed

Lines changed: 207 additions & 1 deletion

File tree

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/FileHeaderCodeFixProvider.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace StyleCop.Analyzers.DocumentationRules
55
{
66
using System;
7+
using System.Collections.Generic;
78
using System.Collections.Immutable;
89
using System.Composition;
910
using System.Text;
@@ -212,6 +213,9 @@ private static SyntaxNode ReplaceHeader(Document document, SyntaxNode root, Styl
212213
var leadingSpaces = string.Empty;
213214
string possibleLeadingSpaces = string.Empty;
214215

216+
// remove header decoration lines, they will be re-generated
217+
trivia = RemoveHeaderDecorationLines(trivia, settings);
218+
215219
// Need to do this with index so we get the line endings correct.
216220
for (int i = 0; i < trivia.Count; i++)
217221
{
@@ -365,16 +369,56 @@ private static string WrapInXmlComment(string prefixWithLeadingSpaces, string co
365369
string encodedCompanyName = new XAttribute("t", settings.DocumentationRules.CompanyName).ToString().Substring(2).Trim('"');
366370
string encodedCopyrightText = new XText(copyrightText).ToString();
367371

368-
return
372+
string copyrightString =
369373
$"{prefixWithLeadingSpaces} <copyright file=\"{encodedFilename}\" company=\"{encodedCompanyName}\">" + newLineText
370374
+ encodedCopyrightText + newLineText
371375
+ prefixWithLeadingSpaces + " </copyright>";
376+
377+
if (!string.IsNullOrEmpty(settings.DocumentationRules.HeaderDecoration))
378+
{
379+
return
380+
$"{prefixWithLeadingSpaces} {settings.DocumentationRules.HeaderDecoration}" + newLineText
381+
+ copyrightString + newLineText
382+
+ $"{prefixWithLeadingSpaces} {settings.DocumentationRules.HeaderDecoration}";
383+
}
384+
385+
return copyrightString;
372386
}
373387

374388
private static string GetCopyrightText(string prefixWithLeadingSpaces, string copyrightText, string newLineText)
375389
{
376390
copyrightText = copyrightText.Replace("\r\n", "\n");
377391
return string.Join(newLineText + prefixWithLeadingSpaces + " ", copyrightText.Split('\n')).Replace(prefixWithLeadingSpaces + " " + newLineText, prefixWithLeadingSpaces + newLineText);
378392
}
393+
394+
private static SyntaxTriviaList RemoveHeaderDecorationLines(SyntaxTriviaList trivia, StyleCopSettings settings)
395+
{
396+
if (!string.IsNullOrEmpty(settings.DocumentationRules.HeaderDecoration))
397+
{
398+
var decorationRemovalList = new List<int>();
399+
for (int i = 0; i < trivia.Count; i++)
400+
{
401+
var triviaLine = trivia[i];
402+
if (triviaLine.Kind() == SyntaxKind.SingleLineCommentTrivia && triviaLine.ToFullString().Contains(settings.DocumentationRules.HeaderDecoration))
403+
{
404+
decorationRemovalList.Add(i);
405+
406+
// also remove the line break
407+
if (i + 1 < trivia.Count && trivia[i + 1].Kind() == SyntaxKind.EndOfLineTrivia)
408+
{
409+
decorationRemovalList.Add(i + 1);
410+
}
411+
}
412+
}
413+
414+
// Remove decoration lines in reverse order.
415+
for (int i = decorationRemovalList.Count - 1; i >= 0; i--)
416+
{
417+
trivia = trivia.RemoveAt(decorationRemovalList[i]);
418+
}
419+
}
420+
421+
return trivia;
422+
}
379423
}
380424
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1633UnitTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,24 @@ public class SA1633UnitTests : FileHeaderTestBase
2424
}
2525
}
2626
}
27+
";
28+
29+
private const string DecoratedXmlMultiLineHeaderTestSettings = @"
30+
{
31+
""settings"": {
32+
""documentationRules"": {
33+
""companyName"": ""FooCorp"",
34+
""copyrightText"": "" Copyright (c) {companyName}. All rights reserved."",
35+
""headerDecoration"": ""-----------------------------------------------------------------------"",
36+
}
37+
}
38+
}
2739
";
2840

2941
private bool useNoXmlSettings;
3042

43+
private bool useDecoratedXmlMultiLineHeaderTestSettings;
44+
3145
/// <summary>
3246
/// Verifies that the analyzer will report <see cref="FileHeaderAnalyzers.SA1633DescriptorMissing"/> for
3347
/// projects using XML headers (the default) when the file is completely missing a header.
@@ -167,6 +181,36 @@ namespace Foo
167181
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
168182
}
169183

184+
/// <summary>
185+
/// Verifies that a file without a header, but with leading trivia will produce the correct diagnostic message.
186+
/// </summary>
187+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
188+
[Fact]
189+
public async Task TestMissingFileHeaderWithDecorationAsync()
190+
{
191+
var testCode = @"namespace Foo
192+
{
193+
}
194+
";
195+
var fixedCode = @"// -----------------------------------------------------------------------
196+
// <copyright file=""Test0.cs"" company=""FooCorp"">
197+
// Copyright (c) FooCorp. All rights reserved.
198+
// </copyright>
199+
// -----------------------------------------------------------------------
200+
201+
namespace Foo
202+
{
203+
}
204+
";
205+
206+
this.useDecoratedXmlMultiLineHeaderTestSettings = true;
207+
208+
var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1633DescriptorMissing).WithLocation(1, 1);
209+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
210+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
211+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
212+
}
213+
170214
/// <summary>
171215
/// Verifies that a file header without XML structure will produce the correct diagnostic message.
172216
/// </summary>
@@ -308,6 +352,11 @@ protected override string GetSettings()
308352
return NoXmlMultiLineHeaderTestSettings;
309353
}
310354

355+
if (this.useDecoratedXmlMultiLineHeaderTestSettings)
356+
{
357+
return DecoratedXmlMultiLineHeaderTestSettings;
358+
}
359+
311360
return base.GetSettings();
312361
}
313362

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1638UnitTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ namespace StyleCop.Analyzers.Test.DocumentationRules
1414
/// </summary>
1515
public class SA1638UnitTests : FileHeaderTestBase
1616
{
17+
private const string DecoratedXmlMultiLineHeaderTestSettings = @"
18+
{
19+
""settings"": {
20+
""documentationRules"": {
21+
""companyName"": ""FooCorp"",
22+
""copyrightText"": "" Copyright (c) {companyName}. All rights reserved."",
23+
""headerDecoration"": ""-----------------------------------------------------------------------"",
24+
}
25+
}
26+
}
27+
";
28+
29+
private bool useDecoratedXmlMultiLineHeaderTestSettings;
30+
1731
/// <summary>
1832
/// Verifies that a file header with a mismatching file attribute in the copyright element will produce the expected diagnostic message.
1933
/// </summary>
@@ -44,9 +58,56 @@ namespace Bar
4458
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
4559
}
4660

61+
/// <summary>
62+
/// Verifies that a file header with a mismatching file attribute in the copyright element will produce the expected diagnostic message.
63+
/// </summary>
64+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
65+
[Fact]
66+
public async Task TestCopyrightElementWithMismatchingFileAttributeAndDecorationAsync()
67+
{
68+
var testCode = @"// -----------------------------------------------------------------------
69+
// <copyright file=""wrongfile.cs"" company=""FooCorp"">
70+
// Copyright (c) FooCorp. All rights reserved.
71+
// </copyright>
72+
// -----------------------------------------------------------------------
73+
74+
namespace Bar
75+
{
76+
}
77+
";
78+
var fixedCode = @"// -----------------------------------------------------------------------
79+
// <copyright file=""Test0.cs"" company=""FooCorp"">
80+
// Copyright (c) FooCorp. All rights reserved.
81+
// </copyright>
82+
// -----------------------------------------------------------------------
83+
84+
namespace Bar
85+
{
86+
}
87+
";
88+
89+
this.useDecoratedXmlMultiLineHeaderTestSettings = true;
90+
91+
var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1638Descriptor).WithLocation(2, 4);
92+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
93+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
94+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
95+
}
96+
4797
protected override CodeFixProvider GetCSharpCodeFixProvider()
4898
{
4999
return new FileHeaderCodeFixProvider();
50100
}
101+
102+
/// <inheritdoc/>
103+
protected override string GetSettings()
104+
{
105+
if (this.useDecoratedXmlMultiLineHeaderTestSettings)
106+
{
107+
return DecoratedXmlMultiLineHeaderTestSettings;
108+
}
109+
110+
return base.GetSettings();
111+
}
51112
}
52113
}

StyleCop.Analyzers/StyleCop.Analyzers/Settings/ObjectModel/DocumentationSettings.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ internal class DocumentationSettings
3737
/// </summary>
3838
private string copyrightTextCache;
3939

40+
/// <summary>
41+
/// This is the backing field for the <see cref="HeaderDecoration"/> property.
42+
/// </summary>
43+
[JsonProperty("headerDecoration", DefaultValueHandling = DefaultValueHandling.Ignore)]
44+
private string headerDecoration;
45+
4046
/// <summary>
4147
/// This is the backing field for the <see cref="Variables"/> property.
4248
/// </summary>
@@ -93,6 +99,7 @@ protected internal DocumentationSettings()
9399
{
94100
this.companyName = DefaultCompanyName;
95101
this.copyrightText = DefaultCopyrightText;
102+
this.headerDecoration = null;
96103
this.variables = ImmutableDictionary<string, string>.Empty.ToBuilder();
97104
this.xmlHeader = true;
98105

@@ -126,6 +133,14 @@ public string CopyrightText
126133
}
127134
}
128135

136+
public string HeaderDecoration
137+
{
138+
get
139+
{
140+
return this.headerDecoration;
141+
}
142+
}
143+
129144
public ImmutableDictionary<string, string> Variables
130145
{
131146
get

StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@
194194
"description": "Determines whether the file header should be wrapped in the StyleCop-standard XML structure.",
195195
"default": true
196196
},
197+
"headerDecoration": {
198+
"type": "string",
199+
"description": "The text used as decoration for the copyright header comment.",
200+
"default": null
201+
},
197202
"fileNamingConvention": {
198203
"type": "string",
199204
"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.",

documentation/Configuration.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ The following properties are used to configure copyright headers in StyleCop Ana
324324
| `copyrightText` | `"Copyright (c) {companyName}. All rights reserved."` | Specifies the default copyright text which should appear in copyright headers |
325325
| `xmlHeader` | **true** | Specifies whether file headers should use standard StyleCop XML format, where the copyright notice is wrapped in a `<copyright>` element |
326326
| `variables` | n/a | Specifies replacement variables which can be referenced in the `copyrightText` value |
327+
| `headerDecoration` | n/a | This value can be set to add a decoration for the header comment so headers look similar to the ones generated by the StyleCop Classic ReSharper fix |
327328

328329
#### Configuring Copyright Text
329330

@@ -373,6 +374,37 @@ When the `xmlHeader` property is explicitly set to **false**, StyleCop Analyzers
373374
// {copyrightText}
374375
```
375376

377+
#### Configuring Copyright Text Header Decoration
378+
379+
The `headerDecoration` property is a string which can contain text that's used for decorating the generated header so
380+
headers look similar to the ones generated by the StyleCop Classic ReSharper fix.
381+
382+
The default value for the `headerDecoration` property is empty, so no decoration will be added.
383+
384+
> :memo: The header decoration is not checked, it's only used for fixing the header.
385+
386+
```json
387+
{
388+
"settings": {
389+
"documentationRules": {
390+
"companyName": "FooCorp",
391+
"copyrightText": "Copyright (c) {companyName}. All rights reserved.",
392+
"headerDecoration": "-----------------------------------------------------------------------"
393+
}
394+
}
395+
}
396+
```
397+
398+
With the above configuration, the fix for a file **TypeName.cs** would look like the following.
399+
400+
```csharp
401+
// -----------------------------------------------------------------------
402+
// <copyright file="TypeName.cs" company="FooCorp">
403+
// Copyright (c) FooCorp. All rights reserved.
404+
// </copyright>
405+
// -----------------------------------------------------------------------
406+
```
407+
376408
### Documentation Requirements
377409

378410
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.

0 commit comments

Comments
 (0)