Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add file header decoration comments if configured #2087

Merged
merged 5 commits into from
Jul 14, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace StyleCop.Analyzers.DocumentationRules
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Text;
Expand Down Expand Up @@ -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++)
{
Expand Down Expand Up @@ -365,16 +369,56 @@ 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} <copyright file=\"{encodedFilename}\" company=\"{encodedCompanyName}\">" + newLineText
+ encodedCopyrightText + newLineText
+ prefixWithLeadingSpaces + " </copyright>";

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)
{
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<int>();
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Verifies that the analyzer will report <see cref="FileHeaderAnalyzers.SA1633DescriptorMissing"/> for
/// projects using XML headers (the default) when the file is completely missing a header.
Expand Down Expand Up @@ -167,6 +181,36 @@ namespace Foo
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
}

/// <summary>
/// Verifies that a file without a header, but with leading trivia will produce the correct diagnostic message.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestMissingFileHeaderWithDecorationAsync()
{
var testCode = @"namespace Foo
{
}
";
var fixedCode = @"// -----------------------------------------------------------------------
// <copyright file=""Test0.cs"" company=""FooCorp"">
// Copyright (c) FooCorp. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

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);
}

/// <summary>
/// Verifies that a file header without XML structure will produce the correct diagnostic message.
/// </summary>
Expand Down Expand Up @@ -308,6 +352,11 @@ protected override string GetSettings()
return NoXmlMultiLineHeaderTestSettings;
}

if (this.useDecoratedXmlMultiLineHeaderTestSettings)
{
return DecoratedXmlMultiLineHeaderTestSettings;
}

return base.GetSettings();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ namespace StyleCop.Analyzers.Test.DocumentationRules
/// </summary>
public class SA1638UnitTests : FileHeaderTestBase
{
private const string DecoratedXmlMultiLineHeaderTestSettings = @"
{
""settings"": {
""documentationRules"": {
""companyName"": ""FooCorp"",
""copyrightText"": "" Copyright (c) {companyName}. All rights reserved."",
""headerDecoration"": ""-----------------------------------------------------------------------"",
}
}
}
";

private bool useDecoratedXmlMultiLineHeaderTestSettings;

/// <summary>
/// Verifies that a file header with a mismatching file attribute in the copyright element will produce the expected diagnostic message.
/// </summary>
Expand Down Expand Up @@ -44,9 +58,56 @@ namespace Bar
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
}

/// <summary>
/// Verifies that a file header with a mismatching file attribute in the copyright element will produce the expected diagnostic message.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestCopyrightElementWithMismatchingFileAttributeAndDecorationAsync()
{
var testCode = @"// -----------------------------------------------------------------------
// <copyright file=""wrongfile.cs"" company=""FooCorp"">
// Copyright (c) FooCorp. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

namespace Bar
{
}
";
var fixedCode = @"// -----------------------------------------------------------------------
// <copyright file=""Test0.cs"" company=""FooCorp"">
// Copyright (c) FooCorp. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

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();
}

/// <inheritdoc/>
protected override string GetSettings()
{
if (this.useDecoratedXmlMultiLineHeaderTestSettings)
{
return DecoratedXmlMultiLineHeaderTestSettings;
}

return base.GetSettings();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ internal class DocumentationSettings
[JsonProperty("copyrightText", DefaultValueHandling = DefaultValueHandling.Ignore)]
private string copyrightText;

/// <summary>
/// This is the backing field for the <see cref="CompanyName"/> property.
/// </summary>
[JsonProperty("headerDecoration", DefaultValueHandling = DefaultValueHandling.Ignore)]
private string headerDecoration;

/// <summary>
/// This is the cache for the <see cref="CopyrightText"/> property.
/// </summary>
Expand Down Expand Up @@ -94,6 +100,7 @@ protected internal DocumentationSettings()
this.companyName = DefaultCompanyName;
this.copyrightText = DefaultCopyrightText;
this.variables = ImmutableDictionary<string, string>.Empty.ToBuilder();
this.headerDecoration = null;
this.xmlHeader = true;

this.documentExposedElements = true;
Expand Down Expand Up @@ -126,6 +133,14 @@ public string CopyrightText
}
}

public string HeaderDecoration
{
get
{
return this.headerDecoration;
}
}

public ImmutableDictionary<string, string> Variables
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@
"description": "Determines whether the file header should be wrapped in the StyleCop-standard XML structure.",
"default": true
},
"headerDecoration": {
"type": "boolean",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not familiar with json schemas, but shouldn't this be string?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are totally right, probably copy+paste :-)

"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.",
Expand Down
30 changes: 30 additions & 0 deletions documentation/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<copyright>` 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

Expand Down Expand Up @@ -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 file="TypeName.cs" company="FooCorp">
// Copyright (c) FooCorp. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
```

### 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.
Expand Down