diff --git a/eng/Versions.props b/eng/Versions.props
index 482556ea91..6afaad04f2 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -15,6 +15,7 @@
0.1.0-alpha-63729-01
0.1.0-alpha-63729-01
1.2.2
+ 1.0.0-beta1-63812-02
2.1.1
2.1.1
1.1.20180503.2
diff --git a/src/CodeFormatter.cs b/src/CodeFormatter.cs
index 11cd06182e..fab54460f6 100644
--- a/src/CodeFormatter.cs
+++ b/src/CodeFormatter.cs
@@ -21,7 +21,8 @@ internal static class CodeFormatter
{
private static readonly ImmutableArray s_codeFormatters = new ICodeFormatter[]
{
- new WhitespaceFormatter()
+ new WhitespaceFormatter(),
+ new EndOfFileNewLineFormatter()
}.ToImmutableArray();
public static async Task FormatWorkspaceAsync(
@@ -163,7 +164,7 @@ void LogWorkspaceWarnings(object sender, WorkspaceDiagnosticEventArgs args)
private static async Task RunCodeFormattersAsync(
Solution solution,
- ImmutableArray<(Document, OptionSet)> formattableDocuments,
+ ImmutableArray<(Document, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
ILogger logger,
CancellationToken cancellationToken)
{
@@ -177,7 +178,7 @@ private static async Task RunCodeFormattersAsync(
return formattedSolution;
}
- internal static async Task<(int, ImmutableArray<(Document, OptionSet)>)> DetermineFormattableFiles(
+ internal static async Task<(int, ImmutableArray<(Document, OptionSet, ICodingConventionsSnapshot)>)> DetermineFormattableFiles(
Solution solution,
string projectPath,
ImmutableHashSet filesToFormat,
@@ -188,7 +189,7 @@ private static async Task RunCodeFormattersAsync(
var optionsApplier = new EditorConfigOptionsApplier();
var fileCount = 0;
- var getDocumentsAndOptions = new List>(solution.Projects.Sum(project => project.DocumentIds.Count));
+ var getDocumentsAndOptions = new List>(solution.Projects.Sum(project => project.DocumentIds.Count));
foreach (var project in solution.Projects)
{
@@ -215,11 +216,11 @@ private static async Task RunCodeFormattersAsync(
}
var documentsAndOptions = await Task.WhenAll(getDocumentsAndOptions).ConfigureAwait(false);
- var foundEditorConfig = documentsAndOptions.Any(documentAndOptions => documentAndOptions.Item3);
+ var foundEditorConfig = documentsAndOptions.Any(documentAndOptions => documentAndOptions.Item4);
var addedFilePaths = new HashSet(documentsAndOptions.Length);
- var formattableFiles = ImmutableArray.CreateBuilder<(Document, OptionSet)>(documentsAndOptions.Length);
- foreach (var (document, options, hasEditorConfig) in documentsAndOptions)
+ var formattableFiles = ImmutableArray.CreateBuilder<(Document, OptionSet, ICodingConventionsSnapshot)>(documentsAndOptions.Length);
+ foreach (var (document, options, codingConventions, hasEditorConfig) in documentsAndOptions)
{
if (document is null)
{
@@ -239,13 +240,13 @@ private static async Task RunCodeFormattersAsync(
}
addedFilePaths.Add(document.FilePath);
- formattableFiles.Add((document, options));
+ formattableFiles.Add((document, options, codingConventions));
}
return (fileCount, formattableFiles.ToImmutableArray());
}
- private static async Task<(Document, OptionSet, bool)> GetDocumentAndOptions(
+ private static async Task<(Document, OptionSet, ICodingConventionsSnapshot, bool)> GetDocumentAndOptions(
Project project,
DocumentId documentId,
ImmutableHashSet filesToFormat,
@@ -258,18 +259,18 @@ private static async Task RunCodeFormattersAsync(
// If a files list was passed in, then ignore files not present in the list.
if (!filesToFormat.IsEmpty && !filesToFormat.Contains(document.FilePath))
{
- return (null, null, false);
+ return (null, null, null, false);
}
if (!document.SupportsSyntaxTree)
{
- return (null, null, false);
+ return (null, null, null, false);
}
// Ignore generated code files.
if (await GeneratedCodeUtilities.IsGeneratedCodeAsync(document, cancellationToken).ConfigureAwait(false))
{
- return (null, null, false);
+ return (null, null, null, false);
}
var context = await codingConventionsManager.GetConventionContextAsync(
@@ -280,11 +281,11 @@ private static async Task RunCodeFormattersAsync(
// Check whether an .editorconfig was found for this document.
if (context?.CurrentConventions is null)
{
- return (document, options, false);
+ return (document, options, null, false);
}
options = optionsApplier.ApplyConventions(options, context.CurrentConventions, project.Language);
- return (document, options, true);
+ return (document, options, context.CurrentConventions, true);
}
}
}
diff --git a/src/Formatters/DocumentFormatter.cs b/src/Formatters/DocumentFormatter.cs
index 658f46b198..f6dca24cf3 100644
--- a/src/Formatters/DocumentFormatter.cs
+++ b/src/Formatters/DocumentFormatter.cs
@@ -7,6 +7,7 @@
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.CodingConventions;
namespace Microsoft.CodeAnalysis.Tools.Formatters
{
@@ -20,7 +21,7 @@ internal abstract class DocumentFormatter : ICodeFormatter
///
public async Task FormatAsync(
Solution solution,
- ImmutableArray<(Document, OptionSet)> formattableDocuments,
+ ImmutableArray<(Document, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
ILogger logger,
CancellationToken cancellationToken)
{
@@ -34,6 +35,7 @@ public async Task FormatAsync(
protected abstract Task FormatFileAsync(
Document document,
OptionSet options,
+ ICodingConventionsSnapshot codingConventions,
ILogger logger,
CancellationToken cancellationToken);
@@ -41,15 +43,15 @@ protected abstract Task FormatFileAsync(
/// Applies formatting and returns the changed for each .
///
private ImmutableArray<(Document, Task)> FormatFiles(
- ImmutableArray<(Document, OptionSet)> formattableDocuments,
+ ImmutableArray<(Document, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
ILogger logger,
CancellationToken cancellationToken)
{
var formattedDocuments = ImmutableArray.CreateBuilder<(Document, Task)>(formattableDocuments.Length);
- foreach (var (document, options) in formattableDocuments)
+ foreach (var (document, options, codingConventions) in formattableDocuments)
{
- var formatTask = Task.Run(async () => await GetFormattedSourceTextAsync(document, options, logger, cancellationToken).ConfigureAwait(false), cancellationToken);
+ var formatTask = Task.Run(async () => await GetFormattedSourceTextAsync(document, options, codingConventions, logger, cancellationToken).ConfigureAwait(false), cancellationToken);
formattedDocuments.Add((document, formatTask));
}
@@ -63,13 +65,14 @@ protected abstract Task FormatFileAsync(
private async Task GetFormattedSourceTextAsync(
Document document,
OptionSet options,
+ ICodingConventionsSnapshot codingConventions,
ILogger logger,
CancellationToken cancellationToken)
{
logger.LogTrace(Resources.Formatting_code_file_0, Path.GetFileName(document.FilePath));
var originalSourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
- var formattedSourceText = await FormatFileAsync(document, options, logger, cancellationToken).ConfigureAwait(false);
+ var formattedSourceText = await FormatFileAsync(document, options, codingConventions, logger, cancellationToken).ConfigureAwait(false);
return !formattedSourceText.ContentEquals(originalSourceText)
? formattedSourceText
diff --git a/src/Formatters/EndOfFileNewlineFormatter.cs b/src/Formatters/EndOfFileNewlineFormatter.cs
new file mode 100644
index 0000000000..11e3047c44
--- /dev/null
+++ b/src/Formatters/EndOfFileNewlineFormatter.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Options;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.CodingConventions;
+
+namespace Microsoft.CodeAnalysis.Tools.Formatters
+{
+ internal sealed class EndOfFileNewLineFormatter : DocumentFormatter
+ {
+ protected override async Task FormatFileAsync(
+ Document document,
+ OptionSet options,
+ ICodingConventionsSnapshot codingConventions,
+ ILogger logger,
+ CancellationToken cancellationToken)
+ {
+ if (!codingConventions.TryGetConventionValue("insert_final_newline", out bool insertFinalNewline))
+ {
+ return await document.GetTextAsync(cancellationToken);
+ }
+
+ var endOfLine = codingConventions.TryGetConventionValue("end_of_line", out string endOfLineOption)
+ ? GetEndOfLine(endOfLineOption)
+ : Environment.NewLine;
+
+ var sourceText = await document.GetTextAsync(cancellationToken);
+ var lastLine = sourceText.Lines.Last();
+
+ var hasFinalNewLine = lastLine.Span.IsEmpty;
+
+ if (insertFinalNewline && !hasFinalNewLine)
+ {
+ var finalNewLineSpan = new TextSpan(lastLine.End, 0);
+ var addNewLineChange = new TextChange(finalNewLineSpan, endOfLine);
+ sourceText = sourceText.WithChanges(addNewLineChange);
+ }
+ else if (!insertFinalNewline && hasFinalNewLine)
+ {
+ // In the case of empty files where there is a single empty line, there is nothing to remove.
+ while (sourceText.Lines.Count > 1 && hasFinalNewLine)
+ {
+ var lineBeforeLast = sourceText.Lines[sourceText.Lines.Count - 2];
+ var finalNewLineSpan = new TextSpan(lineBeforeLast.End, lineBeforeLast.EndIncludingLineBreak - lineBeforeLast.End);
+ var removeNewLineChange = new TextChange(finalNewLineSpan, string.Empty);
+ sourceText = sourceText.WithChanges(removeNewLineChange);
+
+ lastLine = sourceText.Lines.Last();
+ hasFinalNewLine = lastLine.Span.IsEmpty;
+ }
+ }
+
+ return sourceText;
+ }
+
+ private string GetEndOfLine(string endOfLineOption)
+ {
+ switch (endOfLineOption)
+ {
+ case "lf":
+ return "\n";
+ case "cr":
+ return "\r";
+ case "crlf":
+ return "\r\n";
+ default:
+ return Environment.NewLine;
+ }
+ }
+ }
+}
diff --git a/src/Formatters/ICodeFormatter.cs b/src/Formatters/ICodeFormatter.cs
index f3bda36599..70ab7385fb 100644
--- a/src/Formatters/ICodeFormatter.cs
+++ b/src/Formatters/ICodeFormatter.cs
@@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Options;
using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.CodingConventions;
namespace Microsoft.CodeAnalysis.Tools.Formatters
{
@@ -15,7 +16,7 @@ internal interface ICodeFormatter
///
Task FormatAsync(
Solution solution,
- ImmutableArray<(Document, OptionSet)> formattableDocuments,
+ ImmutableArray<(Document, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
ILogger logger,
CancellationToken cancellationToken);
}
diff --git a/src/Formatters/WhitespaceFormatter.cs b/src/Formatters/WhitespaceFormatter.cs
index a9b5e09196..b1f145d151 100644
--- a/src/Formatters/WhitespaceFormatter.cs
+++ b/src/Formatters/WhitespaceFormatter.cs
@@ -6,6 +6,7 @@
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.CodingConventions;
namespace Microsoft.CodeAnalysis.Tools.Formatters
{
@@ -17,6 +18,7 @@ internal sealed class WhitespaceFormatter : DocumentFormatter
protected override async Task FormatFileAsync(
Document document,
OptionSet options,
+ ICodingConventionsSnapshot codingConventions,
ILogger logger,
CancellationToken cancellationToken)
{
diff --git a/tests/Extensions/ExportProviderExtensions.cs b/tests/Extensions/ExportProviderExtensions.cs
new file mode 100644
index 0000000000..a2ec704d2a
--- /dev/null
+++ b/tests/Extensions/ExportProviderExtensions.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.Composition.Hosting.Core;
+using System.Linq;
+using System.Reflection;
+using Microsoft.VisualStudio.Composition;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests
+{
+ internal static class ExportProviderExtensions
+ {
+ public static CompositionContext AsCompositionContext(this ExportProvider exportProvider)
+ {
+ return new CompositionContextShim(exportProvider);
+ }
+
+ private class CompositionContextShim : CompositionContext
+ {
+ private readonly ExportProvider _exportProvider;
+
+ public CompositionContextShim(ExportProvider exportProvider)
+ {
+ _exportProvider = exportProvider;
+ }
+
+ public override bool TryGetExport(CompositionContract contract, out object export)
+ {
+ var importMany = contract.MetadataConstraints.Contains(new KeyValuePair("IsImportMany", true));
+ var (contractType, metadataType) = GetContractType(contract.ContractType, importMany);
+
+ if (metadataType != null)
+ {
+ var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods()
+ where method.Name == nameof(ExportProvider.GetExports)
+ where method.IsGenericMethod && method.GetGenericArguments().Length == 2
+ where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string)
+ select method).Single();
+ var parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType);
+ export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName });
+ }
+ else
+ {
+ var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods()
+ where method.Name == nameof(ExportProvider.GetExports)
+ where method.IsGenericMethod && method.GetGenericArguments().Length == 1
+ where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string)
+ select method).Single();
+ var parameterizedMethod = methodInfo.MakeGenericMethod(contractType);
+ export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName });
+ }
+
+ return true;
+ }
+
+ private (Type exportType, Type metadataType) GetContractType(Type contractType, bool importMany)
+ {
+ if (importMany && contractType.IsConstructedGenericType)
+ {
+ if (contractType.GetGenericTypeDefinition() == typeof(IList<>)
+ || contractType.GetGenericTypeDefinition() == typeof(ICollection<>)
+ || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ {
+ contractType = contractType.GenericTypeArguments[0];
+ }
+ }
+
+ if (contractType.IsConstructedGenericType)
+ {
+ if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>))
+ {
+ return (contractType.GenericTypeArguments[0], null);
+ }
+ else if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>))
+ {
+ return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+ }
+ }
+}
diff --git a/tests/Formatters/AbstractFormatterTest.cs b/tests/Formatters/AbstractFormatterTest.cs
new file mode 100644
index 0000000000..723b98c0b7
--- /dev/null
+++ b/tests/Formatters/AbstractFormatterTest.cs
@@ -0,0 +1,249 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Options;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis.Tools.Formatters;
+using Microsoft.CodeAnalysis.Tools.Tests.Utilities;
+using Microsoft.CodeAnalysis.Tools.Utilities;
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.CodingConventions;
+using Microsoft.VisualStudio.Composition;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.Formatters
+{
+ public abstract class AbstractFormatterTest
+ {
+ private static readonly Lazy ExportProviderFactory;
+
+ static AbstractFormatterTest()
+ {
+ ExportProviderFactory = new Lazy(
+ () =>
+ {
+ var discovery = new AttributedPartDiscovery(Resolver.DefaultInstance, isNonPublicSupported: true);
+ var parts = Task.Run(() => discovery.CreatePartsAsync(MefHostServices.DefaultAssemblies)).GetAwaiter().GetResult();
+ var catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts);
+
+ var configuration = CompositionConfiguration.Create(catalog);
+ var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
+ return runtimeComposition.CreateExportProviderFactory();
+ },
+ LazyThreadSafetyMode.ExecutionAndPublication);
+ }
+
+ protected virtual string DefaultFilePathPrefix { get; } = "Test";
+
+ protected virtual string DefaultTestProjectName { get; } = "TestProject";
+
+ protected virtual string DefaultFilePath => DefaultFilePathPrefix + 0 + "." + DefaultFileExt;
+
+ protected abstract string DefaultFileExt { get; }
+
+ private protected abstract ICodeFormatter Formatter { get; }
+
+ protected AbstractFormatterTest()
+ {
+ TestState = new SolutionState(DefaultFilePathPrefix, DefaultFileExt);
+ }
+
+ ///
+ /// Gets the language name used for the test.
+ ///
+ ///
+ /// The language name used for the test.
+ ///
+ public abstract string Language { get; }
+
+ private string TestCode
+ {
+ set
+ {
+ if (value != null)
+ {
+ TestState.Sources.Add(value);
+ }
+ }
+ }
+
+ private ILogger Logger => new TestLogger();
+ private EditorConfigOptionsApplier OptionsApplier = new EditorConfigOptionsApplier();
+
+ public SolutionState TestState { get; }
+
+ private protected async Task TestAsync(string testCode, string expectedCode, ICodeFormatter formatter, IReadOnlyDictionary editorConfig)
+ {
+ TestCode = testCode;
+
+ var solution = GetSolution(TestState.Sources.ToArray(), TestState.AdditionalFiles.ToArray(), TestState.AdditionalReferences.ToArray());
+ var document = solution.Projects.Single().Documents.Single();
+ var options = (OptionSet)await document.GetOptionsAsync();
+
+ ICodingConventionsSnapshot codingConventions = new TestCodingConventionsSnapshot(editorConfig);
+ options = OptionsApplier.ApplyConventions(options, codingConventions, Language);
+
+ var filesToFormat = new[] { (document, options, codingConventions) }.ToImmutableArray();
+
+ var formattedSolution = await formatter.FormatAsync(solution, filesToFormat, Logger, default);
+ var formattedDocument = formattedSolution.Projects.Single().Documents.Single();
+ var formattedText = await formattedDocument.GetTextAsync();
+
+ Assert.Equal(expectedCode, formattedText.ToString());
+ }
+
+
+ ///
+ /// Gets the collection of inputs to provide to the XML documentation resolver.
+ ///
+ ///
+ /// Files in this collection may be referenced via <include> elements in documentation
+ /// comments.
+ ///
+ public Dictionary XmlReferences { get; } = new Dictionary();
+
+ ///
+ /// Gets a collection of transformation functions to apply to during diagnostic
+ /// or code fix test setup.
+ ///
+ public List> OptionsTransforms { get; } = new List>();
+
+ public Document GetTestDocument(string testCode)
+ {
+ TestCode = testCode;
+ var solution = GetSolution(TestState.Sources.ToArray(), TestState.AdditionalFiles.ToArray(), TestState.AdditionalReferences.ToArray());
+ return solution.Projects.Single().Documents.Single();
+ }
+
+ ///
+ /// Given an array of strings as sources and a language, turn them into a and return the
+ /// solution.
+ ///
+ /// Classes in the form of strings.
+ /// Additional documents to include in the project.
+ /// Additional metadata references to include in the project.
+ /// A solution containing a project with the specified sources and additional files.
+ private Solution GetSolution((string filename, SourceText content)[] sources, (string filename, SourceText content)[] additionalFiles, MetadataReference[] additionalMetadataReferences)
+ {
+ var project = CreateProject(sources, additionalFiles, additionalMetadataReferences, Language);
+ return project.Solution;
+ }
+
+ ///
+ /// Create a project using the input strings as sources.
+ ///
+ ///
+ /// This method first creates a by calling , and then
+ /// applies compilation options to the project by calling .
+ ///
+ /// Classes in the form of strings.
+ /// Additional documents to include in the project.
+ /// Additional metadata references to include in the project.
+ /// The language the source classes are in. Values may be taken from the
+ /// class.
+ /// A created out of the s created from the source
+ /// strings.
+ protected Project CreateProject((string filename, SourceText content)[] sources, (string filename, SourceText content)[] additionalFiles, MetadataReference[] additionalMetadataReferences, string language)
+ {
+ language = language ?? language;
+ return CreateProjectImpl(sources, additionalFiles, additionalMetadataReferences, language);
+ }
+
+ ///
+ /// Create a project using the input strings as sources.
+ ///
+ /// Classes in the form of strings.
+ /// Additional documents to include in the project.
+ /// Additional metadata references to include in the project.
+ /// The language the source classes are in. Values may be taken from the
+ /// class.
+ /// A created out of the s created from the source
+ /// strings.
+ protected virtual Project CreateProjectImpl((string filename, SourceText content)[] sources, (string filename, SourceText content)[] additionalFiles, MetadataReference[] additionalMetadataReferences, string language)
+ {
+ var fileNamePrefix = DefaultFilePathPrefix;
+ var fileExt = DefaultFileExt;
+
+ var projectId = ProjectId.CreateNewId(debugName: DefaultTestProjectName);
+ var solution = CreateSolution(projectId, language);
+
+ solution = solution.AddMetadataReferences(projectId, additionalMetadataReferences);
+
+ for (var i = 0; i < sources.Length; i++)
+ {
+ (var newFileName, var source) = sources[i];
+ var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
+ solution = solution.AddDocument(documentId, newFileName, source);
+ }
+
+ for (var i = 0; i < additionalFiles.Length; i++)
+ {
+ (var newFileName, var source) = additionalFiles[i];
+ var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
+ solution = solution.AddAdditionalDocument(documentId, newFileName, source);
+ }
+
+ return solution.GetProject(projectId);
+ }
+
+ ///
+ /// Creates a solution that will be used as parent for the sources that need to be checked.
+ ///
+ /// The project identifier to use.
+ /// The language for which the solution is being created.
+ /// The created solution.
+ protected virtual Solution CreateSolution(ProjectId projectId, string language)
+ {
+ var compilationOptions = CreateCompilationOptions();
+
+ var xmlReferenceResolver = new TestXmlReferenceResolver();
+ foreach (var xmlReference in XmlReferences)
+ {
+ xmlReferenceResolver.XmlReferences.Add(xmlReference.Key, xmlReference.Value);
+ }
+
+ compilationOptions = compilationOptions.WithXmlReferenceResolver(xmlReferenceResolver);
+
+ var solution = CreateWorkspace()
+ .CurrentSolution
+ .AddProject(projectId, DefaultTestProjectName, DefaultTestProjectName, language)
+ .WithProjectCompilationOptions(projectId, compilationOptions)
+ .AddMetadataReference(projectId, MetadataReferences.CorlibReference)
+ .AddMetadataReference(projectId, MetadataReferences.SystemReference)
+ .AddMetadataReference(projectId, MetadataReferences.SystemCoreReference)
+ .AddMetadataReference(projectId, MetadataReferences.CodeAnalysisReference)
+ .AddMetadataReference(projectId, MetadataReferences.SystemCollectionsImmutableReference);
+
+ if (language == LanguageNames.VisualBasic)
+ {
+ solution = solution.AddMetadataReference(projectId, MetadataReferences.MicrosoftVisualBasicReference);
+ }
+
+ foreach (var transform in OptionsTransforms)
+ {
+ solution.Workspace.Options = transform(solution.Workspace.Options);
+ }
+
+ var parseOptions = solution.GetProject(projectId).ParseOptions;
+ solution = solution.WithProjectParseOptions(projectId, parseOptions.WithDocumentationMode(DocumentationMode.Diagnose));
+
+ return solution;
+ }
+
+ public virtual AdhocWorkspace CreateWorkspace()
+ {
+ var exportProvider = ExportProviderFactory.Value.CreateExportProvider();
+ var host = MefHostServices.Create(exportProvider.AsCompositionContext());
+ return new AdhocWorkspace(host);
+ }
+
+ protected abstract CompilationOptions CreateCompilationOptions();
+ }
+}
diff --git a/tests/Formatters/CSharpFormatterTests.cs b/tests/Formatters/CSharpFormatterTests.cs
new file mode 100644
index 0000000000..e633280a45
--- /dev/null
+++ b/tests/Formatters/CSharpFormatterTests.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.Formatters
+{
+ public abstract class CSharpFormatterTests : AbstractFormatterTest
+ {
+ protected override string DefaultFileExt => "cs";
+
+ public override string Language => LanguageNames.CSharp;
+
+ protected override CompilationOptions CreateCompilationOptions()
+ => new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true);
+ }
+}
diff --git a/tests/Formatters/EofNewLineFormatter.cs b/tests/Formatters/EofNewLineFormatter.cs
new file mode 100644
index 0000000000..c8f5b8101a
--- /dev/null
+++ b/tests/Formatters/EofNewLineFormatter.cs
@@ -0,0 +1,215 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Tools.Formatters;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.Formatters
+{
+ public class EOFNewLineFormatterTests : CSharpFormatterTests
+ {
+ private protected override ICodeFormatter Formatter => new EndOfFileNewLineFormatter();
+
+ [Fact]
+ public async Task WhenFinalNewLineUnspecified_AndFinalNewLineMissing_NoChange()
+ {
+ var testCode = @"
+class C
+{
+}";
+
+ var expectedCode = @"
+class C
+{
+}";
+
+ var editorConfig = new Dictionary()
+ {
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineUnspecified_AndFinalNewLineExits_NoChange()
+ {
+ var testCode = @"
+class C
+{
+}
+";
+
+ var expectedCode = @"
+class C
+{
+}
+";
+
+ var editorConfig = new Dictionary()
+ {
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineRequired_AndEndOfLineIsLineFeed_LineFeedAdded()
+ {
+ var testCode = "class C\n{\n}";
+
+ var expectedCode = "class C\n{\n}\n";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "true",
+ ["end_of_line"] = "lf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineRequired_AndEndOfLineIsCarriageReturnLineFeed_CarriageReturnLineFeedAdded()
+ {
+ var testCode = "class C\r\n{\r\n}";
+
+ var expectedCode = "class C\r\n{\r\n}\r\n";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "true",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineRequired_AndEndOfLineIsCarriageReturn_CarriageReturnAdded()
+ {
+ var testCode = "class C\r{\r}";
+
+ var expectedCode = "class C\r{\r}\r";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "true",
+ ["end_of_line"] = "cr",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+ [Fact]
+ public async Task WhenFinalNewLineRequired_AndFinalNewLineExits_NoChange()
+ {
+ var testCode = @"
+class C
+{
+}
+";
+
+ var expectedCode = @"
+class C
+{
+}
+";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "true",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineUnwanted_AndFinalNewLineExists_CarriageReturnLineFeedRemoved()
+ {
+ var testCode = "class C\r\n{\r\n}\r\n\r\n\r\n";
+
+ var expectedCode = "class C\r\n{\r\n}";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "false",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineUnwanted_AndFinalNewLineExists_LineFeedRemoved()
+ {
+ var testCode = "class C\n{\n}\n\n\n";
+
+ var expectedCode = "class C\n{\n}";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "false",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineUnwanted_AndFinalNewLineExists_CarriageReturnRemoved()
+ {
+ var testCode = "class C\r{\r}\r\r\r";
+
+ var expectedCode = "class C\r{\r}";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "false",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineUnwanted_AndFinalNewLineMissing_NoChange()
+ {
+ var testCode = @"
+class C
+{
+}";
+
+ var expectedCode = @"
+class C
+{
+}";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "false",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+
+ [Fact]
+ public async Task WhenFinalNewLineUnwanted_AndFileIsEmpty_NoChange()
+ {
+ var testCode = @"";
+
+ var expectedCode = @"";
+
+ var editorConfig = new Dictionary()
+ {
+ ["insert_final_newline"] = "false",
+ ["end_of_line"] = "crlf",
+ };
+
+ await TestAsync(testCode, expectedCode, Formatter, editorConfig);
+ }
+ }
+}
diff --git a/tests/MSBuildWorkspaceFinderTests.cs b/tests/MSBuild/MSBuildWorkspaceFinderTests.cs
similarity index 98%
rename from tests/MSBuildWorkspaceFinderTests.cs
rename to tests/MSBuild/MSBuildWorkspaceFinderTests.cs
index 638fb9e2f7..84862589c1 100644
--- a/tests/MSBuildWorkspaceFinderTests.cs
+++ b/tests/MSBuild/MSBuildWorkspaceFinderTests.cs
@@ -6,7 +6,7 @@
using Microsoft.CodeAnalysis.Tools.Tests.Utilities;
using Xunit;
-namespace Microsoft.CodeAnalysis.Tools.Tests
+namespace Microsoft.CodeAnalysis.Tools.Tests.MSBuild
{
public class MSBuildWorkspaceFinderTests : IClassFixture
{
diff --git a/tests/Utilities/SolutionPathFixture.cs b/tests/Utilities/SolutionPathFixture.cs
index 7b328aa11c..1863e03650 100644
--- a/tests/Utilities/SolutionPathFixture.cs
+++ b/tests/Utilities/SolutionPathFixture.cs
@@ -27,7 +27,7 @@ public void SetCurrentDirectory()
public void Dispose()
{
if (Interlocked.Decrement(ref _registered) == 0)
- {
+ {
Environment.CurrentDirectory = _currentDirectory;
_currentDirectory = null;
}
diff --git a/tests/Utilities/TestCodingConventionsSnapshot.cs b/tests/Utilities/TestCodingConventionsSnapshot.cs
new file mode 100644
index 0000000000..fffcdf15c7
--- /dev/null
+++ b/tests/Utilities/TestCodingConventionsSnapshot.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.VisualStudio.CodingConventions;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.Utilities
+{
+ public class TestCodingConventionsSnapshot : ICodingConventionsSnapshot
+ {
+ public IUniversalCodingConventions UniversalConventions => throw new NotImplementedException();
+
+ public IReadOnlyDictionary AllRawConventions { get; }
+
+ public int Version => 1;
+
+ public TestCodingConventionsSnapshot(IReadOnlyDictionary conventions)
+ {
+ AllRawConventions = conventions.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value);
+ }
+
+ public bool TryGetConventionValue(string conventionName, out T conventionValue)
+ {
+ if (!AllRawConventions.ContainsKey(conventionName))
+ {
+ conventionValue = default;
+ return false;
+ }
+
+ var value = AllRawConventions[conventionName];
+
+ if (typeof(T) == typeof(bool))
+ {
+ conventionValue = (T)(object)Convert.ToBoolean(value);
+ }
+ else
+ {
+ conventionValue = (T)value;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/tests/Utilities/XmlReferenceResolver.cs b/tests/Utilities/XmlReferenceResolver.cs
new file mode 100644
index 0000000000..26c6c3cab3
--- /dev/null
+++ b/tests/Utilities/XmlReferenceResolver.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.Utilities
+{
+ internal class TestXmlReferenceResolver : XmlReferenceResolver
+ {
+ public Dictionary XmlReferences { get; } =
+ new Dictionary();
+
+ public override bool Equals(object other)
+ {
+ return ReferenceEquals(this, other);
+ }
+
+ public override int GetHashCode()
+ {
+ return RuntimeHelpers.GetHashCode(this);
+ }
+
+ public override Stream OpenRead(string resolvedPath)
+ {
+ if (!XmlReferences.TryGetValue(resolvedPath, out var content))
+ {
+ return null;
+ }
+
+ return new MemoryStream(Encoding.UTF8.GetBytes(content));
+ }
+
+ public override string ResolveReference(string path, string baseFilePath)
+ {
+ return path;
+ }
+ }
+}
diff --git a/tests/dotnet-format.UnitTests.csproj b/tests/dotnet-format.UnitTests.csproj
index 4ade99c7e4..34b4542cb7 100644
--- a/tests/dotnet-format.UnitTests.csproj
+++ b/tests/dotnet-format.UnitTests.csproj
@@ -15,6 +15,7 @@
+
diff --git a/tests/projects/for_code_formatter/formatted_project/Program.cs b/tests/projects/for_code_formatter/formatted_project/Program.cs
index e4cfa76c00..8fc2d56cfc 100644
--- a/tests/projects/for_code_formatter/formatted_project/Program.cs
+++ b/tests/projects/for_code_formatter/formatted_project/Program.cs
@@ -9,4 +9,4 @@ static void Main(string[] args)
Console.WriteLine("Hello World!");
}
}
-}
+}
\ No newline at end of file