Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
vweijsters committed Aug 22, 2015
1 parent 6ec18fd commit a3eb863
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace StyleCop.Analyzers.Test.Settings
{
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using StyleCop.Analyzers.DocumentationRules;
using StyleCop.Analyzers.Settings;
using TestHelper;
using Xunit;

/// <summary>
/// Unit tests for the <see cref="SettingsFileCodeFixProvider"/>
/// </summary>
public class SettingsFileCodeFixProviderUnitTests : CodeFixVerifier
{
private const string StyleCopSettingsFileName = "stylecop.json";
private const string TestCode = @"
namespace Foo
{
}
";

private Project originalProject;
private bool createSettingsFile;

/// <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 TestMissingFileHeaderWithLeadingTriviaAsync()
{
this.createSettingsFile = false;

var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(1, 1).WithArguments("is missing or not located at the top of the file.");
await this.VerifyCSharpDiagnosticAsync(TestCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false);

// verify that the codefix does not alter the document
await this.VerifyCSharpFixAsync(TestCode, TestCode).ConfigureAwait(false);
}

/// <summary>
/// Verifies that a code fix will be offered if the settings file does not exist.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestSettingsFileDoesNotExistAsync()
{
this.createSettingsFile = false;

var offeredFixes = await this.GetOfferedCSharpFixesAsync(TestCode).ConfigureAwait(false);
Assert.Equal(1, offeredFixes.Length);
}

/// <summary>
/// Verifies that a code fix will not be offered if the settings file is already present.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestSettingsFileAlreadyExistsAsync()
{
this.createSettingsFile = true;

var offeredFixes = await this.GetOfferedCSharpFixesAsync(TestCode).ConfigureAwait(false);
Assert.Empty(offeredFixes);
}

/// <inheritdoc/>
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
{
yield return new SA1633FileMustHaveHeader();
}

/// <inheritdoc/>
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return new SettingsFileCodeFixProvider();
}

/// <inheritdoc/>
protected override Project CreateProject(string[] sources, string language = "C#", string[] filenames = null)
{
this.originalProject = base.CreateProject(sources, language, filenames);
if (this.createSettingsFile)
{
this.originalProject = this.originalProject.AddAdditionalDocument(StyleCopSettingsFileName, string.Empty).Project;
}

return this.originalProject;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@
<Compile Include="ReadabilityRules\SA1124UnitTests.cs" />
<Compile Include="ReadabilityRules\SA1125UnitTests.cs" />
<Compile Include="ReadabilityRules\SA1126UnitTests.cs" />
<Compile Include="Settings\SettingsFileCodeFixProviderUnitTests.cs" />
<Compile Include="SpacingRules\NumberSignSpacingTestBase.cs" />
<Compile Include="SpacingRules\SA1000UnitTests.cs" />
<Compile Include="SpacingRules\SA1001UnitTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ protected virtual CodeFixProvider GetCSharpCodeFixProvider()
await this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics, maxNumberOfIterations, GetFixAllAnalyzerDocumentAsync, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Gets all offered code fixes for the specified diagnostic within the given source.
/// </summary>
/// <param name="source">A valid C# source file in the form of a string.</param>
/// <param name="diagnosticIndex">Index determining which diagnostic to use for determining the offered code fixes. Uses the first diagnostic if null.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that the task will observe.</param>
/// <returns>The collection of offered code actions. This collection may be empty.</returns>
protected async Task<ImmutableArray<CodeAction>> GetOfferedCSharpFixesAsync(string source, int? diagnosticIndex = null, CancellationToken cancellationToken = default(CancellationToken))
{
return await this.GetOfferedFixesInternalAsync(LanguageNames.CSharp, source, diagnosticIndex, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
protected override Solution CreateSolution(ProjectId projectId, string language)
{
Expand Down Expand Up @@ -292,5 +304,21 @@ private async Task VerifyFixInternalAsync(string language, ImmutableArray<Diagno
var actual = await GetStringFromDocumentAsync(document, cancellationToken).ConfigureAwait(false);
Assert.Equal(newSource, actual);
}

private async Task<ImmutableArray<CodeAction>> GetOfferedFixesInternalAsync(string language, string source, int? diagnosticIndex, ImmutableArray<DiagnosticAnalyzer> analyzers, CodeFixProvider codeFixProvider, CancellationToken cancellationToken)
{
var document = this.CreateDocument(source, language);
var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, new[] { document }, cancellationToken).ConfigureAwait(false);

var index = diagnosticIndex.HasValue ? diagnosticIndex.Value : 0;

Assert.True(index < analyzerDiagnostics.Count());

var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostics[index], (a, d) => actions.Add(a), cancellationToken);
await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false);

return actions.ToImmutableArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace StyleCop.Analyzers.Settings
{
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using StyleCop.Analyzers.DocumentationRules;
using StyleCop.Analyzers.Helpers;

/// <summary>
/// Implements a codefix that will generate a StyleCop settings file if it does not exist yet.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SettingsFileCodeFixProvider))]
[Shared]
public class SettingsFileCodeFixProvider : CodeFixProvider
{
private const string StyleCopSettingsFileName = "stylecop.json";
private const string DefaultSettingsFileContent = @"{
""settings"": {
""documentationRules"": {
""companyName"": ""PlaceholderCompany""
}
}
}
";

/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(
SA1633FileMustHaveHeader.DiagnosticId,
SA1634FileHeaderMustShowCopyright.DiagnosticId,
SA1635FileHeaderMustHaveCopyrightText.DiagnosticId,
SA1636FileHeaderCopyrightTextMustMatch.DiagnosticId,
SA1637FileHeaderMustContainFileName.DiagnosticId,
SA1638FileHeaderFileNameDocumentationMustMatchFileName.DiagnosticId,
SA1639FileHeaderMustHaveSummary.DiagnosticId,
SA1640FileHeaderMustHaveValidCompanyText.DiagnosticId,
SA1641FileHeaderCompanyNameTextMustMatch.DiagnosticId,
SA1649FileHeaderFileNameDocumentationMustMatchTypeName.DiagnosticId);

/// <inheritdoc/>
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var project = context.Document.Project;
var workspace = project.Solution.Workspace;

// check if the settings file already exists
if (project.AdditionalDocuments.Any(IsStyleCopSettingsDocument))
{
return SpecializedTasks.CompletedTask;
}

// check if we are allowed to add it
if (!workspace.CanApplyChange(ApplyChangesKind.AddAdditionalDocument))
{
return SpecializedTasks.CompletedTask;
}

foreach (var diagnostic in context.Diagnostics.Where(d => this.FixableDiagnosticIds.Contains(d.Id)))
{
context.RegisterCodeFix(CodeAction.Create(SettingsResources.SettingsFileCodeFix, token => GetTransformedSolutionAsync(context.Document, diagnostic, token), equivalenceKey: nameof(SettingsFileCodeFixProvider)), diagnostic);
}

return SpecializedTasks.CompletedTask;
}

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider()
{
// Added this to make it explicitly clear that this codefix does not support fix all actions.
return null;
}

private static bool IsStyleCopSettingsDocument(TextDocument document)
{
return string.Equals(document.Name, StyleCopSettingsFileName, StringComparison.OrdinalIgnoreCase);
}

private static Task<Solution> GetTransformedSolutionAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var project = document.Project;
var solution = project.Solution;

var newDocumentId = DocumentId.CreateNewId(project.Id);

var newSolution = solution.AddAdditionalDocument(newDocumentId, StyleCopSettingsFileName, DefaultSettingsFileContent);

return Task.FromResult(newSolution);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a3eb863

Please sign in to comment.