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