Skip to content

Commit 31084f0

Browse files
committed
Fix so for well formed copyright headers so only the copyright tag is updated. On malformed headers the whole header is replaced.
1 parent dde9766 commit 31084f0

4 files changed

Lines changed: 176 additions & 35 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1634UnitTests.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ namespace Bar
9696
var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1634Descriptor).WithLocation(1, 1);
9797
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
9898
}
99+
99100
/// <summary>
100-
/// Verifies that a file header with a copyright element will not produce a diagnostic message.
101+
/// Check multiple line copyright headers can be checked correctly.
101102
/// </summary>
102103
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
103104
[Fact]
@@ -108,6 +109,7 @@ public async Task TestValidMultiLineFileHeaderWithCopyrightLastAsync()
108109
// </author>
109110
// <copyright file=""Test0.cs"" company=""FooCorp"">
110111
// Copyright (c) FooCorp. All rights reserved.
112+
//
111113
// Licence is FooBar MIT.
112114
// </copyright>
113115
@@ -120,17 +122,52 @@ namespace Bar
120122
""settings"": {
121123
""documentationRules"": {
122124
""companyName"": ""FooCorp"",
123-
""copyrightText"": ""Copyright (c) FooCorp. All rights reserved.\nLicence is FooBar MIT.""
125+
""copyrightText"": ""Copyright (c) FooCorp. All rights reserved.\n\nLicence is FooBar MIT.""
124126
}
125127
}
126128
}
127129
";
128130
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
129131
}
130132

133+
/// <summary>
134+
/// Verifies that a file header with missing copyright text the fix leaves behind other comments.
135+
/// </summary>
136+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
137+
[Fact]
138+
public async Task TestFileHeaderFixWithReplaceCopyrightTagTextAsync()
139+
{
140+
var testCode = @"// <author>
141+
// John Doe
142+
// </author>
143+
// <summary>This is a test file.</summary>
144+
145+
namespace Bar
146+
{
147+
}
148+
";
149+
var fixedCode = @"// <copyright file=""Test0.cs"" company=""FooCorp"">
150+
// Copyright (c) FooCorp. All rights reserved.
151+
// </copyright>
152+
// <author>
153+
// John Doe
154+
// </author>
155+
// <summary>This is a test file.</summary>
156+
157+
namespace Bar
158+
{
159+
}
160+
";
161+
162+
var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1634Descriptor).WithLocation(1, 1);
163+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
164+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
165+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
166+
}
167+
131168
protected override CodeFixProvider GetCSharpCodeFixProvider()
132169
{
133-
throw new System.NotImplementedException();
170+
return new FileHeaderCodeFixProvider();
134171
}
135172

136173
protected override string GetSettings()

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1636UnitTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,44 @@ namespace Bar
7474
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
7575
}
7676

77+
/// <summary>
78+
/// Verifies that a file header with an incorrect copyright text the fix only replaces the text.
79+
/// </summary>
80+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
81+
[Fact]
82+
public async Task TestFileHeaderFixWithReplaceCopyrightTagTextAsync()
83+
{
84+
var testCode = @"// <author>
85+
// John Doe
86+
// </author>
87+
// <copyright file=""Test0.cs"" company=""FooCorp"">
88+
// Copyright (c) Not FooCorp. All rights reserved.
89+
// </copyright>
90+
// <summary>This is a test file.</summary>
91+
92+
namespace Bar
93+
{
94+
}
95+
";
96+
var fixedCode = @"// <author>
97+
// John Doe
98+
// </author>
99+
// <copyright file=""Test0.cs"" company=""FooCorp"">
100+
// Copyright (c) FooCorp. All rights reserved.
101+
// </copyright>
102+
// <summary>This is a test file.</summary>
103+
104+
namespace Bar
105+
{
106+
}
107+
";
108+
109+
var expectedDiagnostic = this.CSharpDiagnostic(FileHeaderAnalyzers.SA1636Descriptor).WithLocation(4, 4);
110+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);
111+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
112+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
113+
}
114+
77115
protected override CodeFixProvider GetCSharpCodeFixProvider()
78116
{
79117
return new FileHeaderCodeFixProvider();

StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/FileHeaderAnalyzers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ private static void CheckCopyrightText(SyntaxTreeAnalysisContext context, Compil
323323
}
324324

325325
// Remove any leading or trailing spaces and remove the extra space put in as a seperator.
326-
if (string.CompareOrdinal(copyrightText.Trim(' ', '\r', '\n').Replace("\n ","\n"), settingsCopyrightText.Trim(' ', '\r', '\n')) != 0)
326+
if (string.CompareOrdinal(copyrightText.Trim(' ', '\r', '\n').Replace("\n ", "\n"), settingsCopyrightText.Trim(' ', '\r', '\n')) != 0)
327327
{
328328
var location = fileHeader.GetElementLocation(context.Tree, copyrightElement);
329329
context.ReportDiagnostic(Diagnostic.Create(SA1636Descriptor, location));

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

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class FileHeaderCodeFixProvider : CodeFixProvider
3030
public override ImmutableArray<string> FixableDiagnosticIds { get; }
3131
= ImmutableArray.Create(
3232
FileHeaderAnalyzers.SA1633DescriptorMissing.Id,
33+
FileHeaderAnalyzers.SA1634Descriptor.Id,
3334
FileHeaderAnalyzers.SA1635Descriptor.Id,
3435
FileHeaderAnalyzers.SA1636Descriptor.Id);
3536

@@ -56,58 +57,123 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
5657
var settings = document.Project.AnalyzerOptions.GetStyleCopSettings();
5758

5859
var fileHeader = FileHeaderHelpers.ParseFileHeader(root);
59-
var newSyntaxRoot = fileHeader.IsMissing ? AddHeader(document, root, document.Name, settings) : ReplaceHeader(document, root, settings);
60+
var xmlFileHeader = FileHeaderHelpers.ParseXmlFileHeader(root);
61+
var newSyntaxRoot = fileHeader.IsMissing ? AddHeader(document, root, document.Name, settings) : ReplaceHeader(document, root, settings, xmlFileHeader.IsMalformed);
6062

6163
return document.WithSyntaxRoot(newSyntaxRoot);
6264
}
6365

64-
private static SyntaxNode ReplaceHeader(Document document, SyntaxNode root, StyleCopSettings settings)
66+
private static SyntaxNode ReplaceHeader(Document document, SyntaxNode root, StyleCopSettings settings, bool isMalformedHeader)
6567
{
68+
// If the header is well formed Xml then we parse out the copyright otherwise
6669
// Skip single line comments, whitespace, and end of line trivia until a blank line is encountered.
6770
SyntaxTriviaList trivia = root.GetLeadingTrivia();
68-
6971
bool onBlankLine = false;
70-
while (trivia.Any())
72+
bool inCopyright = isMalformedHeader;
73+
int? copyrightTriviaIndex = null;
74+
var removalList = new System.Collections.Generic.List<int>();
75+
76+
// Need to do this with index so we get the line endings correct.
77+
for (int i = 0; i < trivia.Count; i++)
7178
{
79+
var triviaLine = trivia[i];
7280
bool done = false;
73-
switch (trivia[0].Kind())
81+
switch (triviaLine.Kind())
7482
{
75-
case SyntaxKind.SingleLineCommentTrivia:
76-
trivia = trivia.RemoveAt(0);
77-
onBlankLine = false;
78-
break;
83+
case SyntaxKind.SingleLineCommentTrivia:
84+
if (!isMalformedHeader)
85+
{
86+
var openingTag = triviaLine.ToFullString().Contains("<copyright ");
87+
var closingTag = triviaLine.ToFullString().Contains("</copyright>") ||
88+
(openingTag && triviaLine.ToFullString().Trim().EndsWith("/>"));
89+
if (openingTag)
90+
{
91+
inCopyright = !closingTag;
92+
copyrightTriviaIndex = i;
93+
}
94+
else
95+
{
96+
if (inCopyright)
97+
{
98+
removalList.Add(i);
99+
inCopyright = !closingTag;
100+
}
101+
}
102+
}
103+
else
104+
{
105+
removalList.Add(i);
106+
}
107+
108+
onBlankLine = false;
109+
break;
110+
111+
case SyntaxKind.WhitespaceTrivia:
112+
removalList.Add(i);
113+
break;
114+
115+
case SyntaxKind.EndOfLineTrivia:
116+
if (inCopyright)
117+
{
118+
removalList.Add(i);
119+
}
120+
121+
if (onBlankLine)
122+
{
123+
done = true;
124+
}
125+
else
126+
{
127+
onBlankLine = true;
128+
}
129+
130+
break;
131+
132+
default:
133+
done = true;
134+
break;
135+
}
79136

80-
case SyntaxKind.WhitespaceTrivia:
81-
trivia = trivia.RemoveAt(0);
137+
if (done)
138+
{
82139
break;
140+
}
141+
}
83142

84-
case SyntaxKind.EndOfLineTrivia:
85-
trivia = trivia.RemoveAt(0);
143+
if (isMalformedHeader)
144+
{
145+
copyrightTriviaIndex = null;
146+
}
86147

87-
if (onBlankLine)
88-
{
89-
done = true;
90-
}
91-
else
92-
{
93-
onBlankLine = true;
94-
}
148+
// Remove copyright lines in reverse order
149+
removalList.Reverse();
150+
foreach (var triviaLine in removalList)
151+
{
152+
trivia = trivia.RemoveAt(triviaLine);
153+
}
95154

96-
break;
155+
string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
97156

98-
default:
99-
done = true;
100-
break;
157+
var newHeaderTrivia = CreateNewHeader(document.Name, settings, newLineText);
158+
if (copyrightTriviaIndex.HasValue)
159+
{
160+
if (inCopyright)
161+
{
162+
newHeaderTrivia = newHeaderTrivia.Add(SyntaxFactory.CarriageReturnLineFeed);
101163
}
102164

103-
if (done)
165+
trivia = trivia.ReplaceRange(trivia[copyrightTriviaIndex.Value], newHeaderTrivia);
166+
return root.WithLeadingTrivia(trivia);
167+
}
168+
else
169+
{
170+
if ((trivia.Count == 0) || (trivia[0].Kind() != SyntaxKind.SingleLineCommentTrivia))
104171
{
105-
break;
172+
newHeaderTrivia = newHeaderTrivia.Add(SyntaxFactory.CarriageReturnLineFeed);
106173
}
107-
}
108174

109-
string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
110-
return root.WithLeadingTrivia(CreateNewHeader(document.Name, settings, newLineText).Add(SyntaxFactory.CarriageReturnLineFeed).Add(SyntaxFactory.CarriageReturnLineFeed).AddRange(trivia));
175+
return root.WithLeadingTrivia(newHeaderTrivia.Add(SyntaxFactory.CarriageReturnLineFeed).AddRange(trivia));
176+
}
111177
}
112178

113179
private static SyntaxNode AddHeader(Document document, SyntaxNode root, string name, StyleCopSettings settings)
@@ -139,7 +205,7 @@ private static string WrapInXmlComment(string copyrightText, string filename, St
139205
private static string GetCopyrightText(string copyrightText, string newLineText)
140206
{
141207
copyrightText = copyrightText.Replace("\r\n", "\n");
142-
return string.Join(newLineText + "// ", copyrightText.Split('\n'));
208+
return string.Join(newLineText + "// ", copyrightText.Split('\n')).Replace("// " + newLineText, "//" + newLineText);
143209
}
144210
}
145211
}

0 commit comments

Comments
 (0)