From 9cefcdd4501bee5a424227a50f317bcc839ccedc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 4 Dec 2016 23:30:34 -0800 Subject: [PATCH] Add legacy dependencies of C# lowering --- .../Legacy/CSharpCodeWriter.cs | 536 ++++++++++++++++++ .../Legacy/CSharpCodeWritingScope.cs | 71 +++ .../Legacy/CSharpDisableWarningScope.cs | 29 + .../Legacy/CodeWriter.cs | 203 +++++++ .../Legacy/LineMapping.cs | 79 +++ .../Legacy/LineMappingManager.cs | 22 + .../Legacy/MappingLocation.cs | 105 ++++ .../Legacy/CSharpCodeWriterTest.cs | 47 ++ .../Legacy/CodeWriterTest.cs | 265 +++++++++ .../Legacy/LineMappingTest.cs | 123 ++++ 10 files changed, 1480 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWritingScope.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpDisableWarningScope.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CodeWriter.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMapping.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMappingManager.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/MappingLocation.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeWriterTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineMappingTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs new file mode 100644 index 0000000000..47c21ab977 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs @@ -0,0 +1,536 @@ +// Copyright (c) .NET Foundation. 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.Diagnostics; +using System.Globalization; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpCodeWriter : CodeWriter + { + private const string InstanceMethodFormat = "{0}.{1}"; + + private static readonly char[] CStyleStringLiteralEscapeChars = { + '\r', + '\t', + '\"', + '\'', + '\\', + '\0', + '\n', + '\u2028', + '\u2029', + }; + + public CSharpCodeWriter() + { + LineMappingManager = new LineMappingManager(); + } + + public LineMappingManager LineMappingManager { get; private set; } + + public new CSharpCodeWriter Write(string data) + { + return (CSharpCodeWriter)base.Write(data); + } + + public new CSharpCodeWriter Indent(int size) + { + return (CSharpCodeWriter)base.Indent(size); + } + + public new CSharpCodeWriter ResetIndent() + { + return (CSharpCodeWriter)base.ResetIndent(); + } + + public new CSharpCodeWriter SetIndent(int size) + { + return (CSharpCodeWriter)base.SetIndent(size); + } + + public new CSharpCodeWriter IncreaseIndent(int size) + { + return (CSharpCodeWriter)base.IncreaseIndent(size); + } + + public new CSharpCodeWriter DecreaseIndent(int size) + { + return (CSharpCodeWriter)base.DecreaseIndent(size); + } + + public new CSharpCodeWriter WriteLine(string data) + { + return (CSharpCodeWriter)base.WriteLine(data); + } + + public new CSharpCodeWriter WriteLine() + { + return (CSharpCodeWriter)base.WriteLine(); + } + + public CSharpCodeWriter WriteVariableDeclaration(string type, string name, string value) + { + Write(type).Write(" ").Write(name); + if (!string.IsNullOrEmpty(value)) + { + Write(" = ").Write(value); + } + else + { + Write(" = null"); + } + + WriteLine(";"); + + return this; + } + + public CSharpCodeWriter WriteComment(string comment) + { + return Write("// ").WriteLine(comment); + } + + public CSharpCodeWriter WriteBooleanLiteral(bool value) + { + return Write(value.ToString().ToLowerInvariant()); + } + + public CSharpCodeWriter WriteStartAssignment(string name) + { + return Write(name).Write(" = "); + } + + public CSharpCodeWriter WriteParameterSeparator() + { + return Write(", "); + } + + public CSharpCodeWriter WriteStartNewObject(string typeName) + { + return Write("new ").Write(typeName).Write("("); + } + + public CSharpCodeWriter WriteLocationTaggedString(LocationTagged value) + { + WriteStringLiteral(value.Value); + WriteParameterSeparator(); + Write(value.Location.AbsoluteIndex.ToString(CultureInfo.InvariantCulture)); + + return this; + } + + public CSharpCodeWriter WriteStringLiteral(string literal) + { + if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1) + { + WriteVerbatimStringLiteral(literal); + } + else + { + WriteCStyleStringLiteral(literal); + } + + return this; + } + + public CSharpCodeWriter WriteLineHiddenDirective() + { + return WriteLine("#line hidden"); + } + + public CSharpCodeWriter WritePragma(string value) + { + return Write("#pragma ").WriteLine(value); + } + + public CSharpCodeWriter WriteUsing(string name) + { + return WriteUsing(name, endLine: true); + } + + public CSharpCodeWriter WriteUsing(string name, bool endLine) + { + Write("using "); + Write(name); + + if (endLine) + { + WriteLine(";"); + } + + return this; + } + + public CSharpCodeWriter WriteLineDefaultDirective() + { + return WriteLine("#line default"); + } + + public CSharpCodeWriter WriteStartReturn() + { + return Write("return "); + } + + public CSharpCodeWriter WriteReturn(string value) + { + return WriteReturn(value, endLine: true); + } + + public CSharpCodeWriter WriteReturn(string value, bool endLine) + { + Write("return ").Write(value); + + if (endLine) + { + Write(";"); + } + + return WriteLine(); + } + + /// + /// Writes a #line pragma directive for the line number at the specified . + /// + /// The location to generate the line pragma for. + /// The file to generate the line pragma for. + /// The current instance of . + public CSharpCodeWriter WriteLineNumberDirective(SourceLocation location, string file) + { + if (location.FilePath != null) + { + file = location.FilePath; + } + + if (Builder.Length >= NewLine.Length && !IsAfterNewLine) + { + WriteLine(); + } + + var lineNumberAsString = (location.LineIndex + 1).ToString(CultureInfo.InvariantCulture); + return Write("#line ").Write(lineNumberAsString).Write(" \"").Write(file).WriteLine("\""); + } + + public CSharpCodeWriter WriteStartMethodInvocation(string methodName) + { + return WriteStartMethodInvocation(methodName, new string[0]); + } + + public CSharpCodeWriter WriteStartMethodInvocation(string methodName, params string[] genericArguments) + { + Write(methodName); + + if (genericArguments.Length > 0) + { + Write("<").Write(string.Join(", ", genericArguments)).Write(">"); + } + + return Write("("); + } + + public CSharpCodeWriter WriteEndMethodInvocation() + { + return WriteEndMethodInvocation(endLine: true); + } + + public CSharpCodeWriter WriteEndMethodInvocation(bool endLine) + { + Write(")"); + if (endLine) + { + WriteLine(";"); + } + + return this; + } + + // Writes a method invocation for the given instance name. + public CSharpCodeWriter WriteInstanceMethodInvocation(string instanceName, + string methodName, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteInstanceMethodInvocation(instanceName, methodName, endLine: true, parameters: parameters); + } + + // Writes a method invocation for the given instance name. + public CSharpCodeWriter WriteInstanceMethodInvocation(string instanceName, + string methodName, + bool endLine, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteMethodInvocation( + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName), + endLine, + parameters); + } + + public CSharpCodeWriter WriteStartInstanceMethodInvocation(string instanceName, + string methodName) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteStartMethodInvocation( + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName)); + } + + public CSharpCodeWriter WriteMethodInvocation(string methodName, params string[] parameters) + { + return WriteMethodInvocation(methodName, endLine: true, parameters: parameters); + } + + public CSharpCodeWriter WriteMethodInvocation(string methodName, bool endLine, params string[] parameters) + { + return WriteStartMethodInvocation(methodName) + .Write(string.Join(", ", parameters)) + .WriteEndMethodInvocation(endLine); + } + + public CSharpCodeWriter WriteAutoPropertyDeclaration(string accessibility, string typeName, string name) + { + return Write(accessibility) + .Write(" ") + .Write(typeName) + .Write(" ") + .Write(name) + .Write(" { get; set; }") + .WriteLine(); + } + + public CSharpDisableWarningScope BuildDisableWarningScope(int warning) + { + return new CSharpDisableWarningScope(this, warning); + } + + public CSharpCodeWritingScope BuildScope() + { + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildLambda(bool endLine, params string[] parameterNames) + { + return BuildLambda(endLine, async: false, parameterNames: parameterNames); + } + + public CSharpCodeWritingScope BuildAsyncLambda(bool endLine, params string[] parameterNames) + { + return BuildLambda(endLine, async: true, parameterNames: parameterNames); + } + + private CSharpCodeWritingScope BuildLambda(bool endLine, bool async, string[] parameterNames) + { + if (async) + { + Write("async"); + } + + Write("(").Write(string.Join(", ", parameterNames)).Write(") => "); + + var scope = new CSharpCodeWritingScope(this); + + if (endLine) + { + // End the lambda with a semicolon + scope.OnClose += () => + { + WriteLine(";"); + }; + } + + return scope; + } + + public CSharpCodeWritingScope BuildNamespace(string name) + { + Write("namespace ").WriteLine(name); + + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildClassDeclaration(string accessibility, string name) + { + return BuildClassDeclaration(accessibility, name, Enumerable.Empty()); + } + + public CSharpCodeWritingScope BuildClassDeclaration(string accessibility, string name, string baseType) + { + return BuildClassDeclaration(accessibility, name, new string[] { baseType }); + } + + public CSharpCodeWritingScope BuildClassDeclaration( + string accessibility, + string name, + IEnumerable baseTypes) + { + Write(accessibility).Write(" class ").Write(name); + + if (baseTypes.Count() > 0) + { + Write(" : "); + Write(string.Join(", ", baseTypes)); + } + + WriteLine(); + + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildConstructor(string name) + { + return BuildConstructor("public", name); + } + + public CSharpCodeWritingScope BuildConstructor(string accessibility, string name) + { + return BuildConstructor(accessibility, name, Enumerable.Empty>()); + } + + public CSharpCodeWritingScope BuildConstructor( + string accessibility, + string name, + IEnumerable> parameters) + { + Write(accessibility) + .Write(" ") + .Write(name) + .Write("(") + .Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))) + .WriteLine(")"); + + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildMethodDeclaration(string accessibility, string returnType, string name) + { + return BuildMethodDeclaration(accessibility, returnType, name, Enumerable.Empty>()); + } + + public CSharpCodeWritingScope BuildMethodDeclaration( + string accessibility, + string returnType, + string name, + IEnumerable> parameters) + { + Write(accessibility) + .Write(" ") + .Write(returnType) + .Write(" ") + .Write(name) + .Write("(") + .Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))) + .WriteLine(")"); + + return new CSharpCodeWritingScope(this); + } + + private void WriteVerbatimStringLiteral(string literal) + { + Write("@\""); + + // We need to find the index of each '"' (double-quote) to escape it. + var start = 0; + int end; + while ((end = literal.IndexOf('\"', start)) > -1) + { + Write(literal, start, end - start); + + Write("\"\""); + + start = end + 1; + } + + Debug.Assert(end == -1); // We've hit all of the double-quotes. + + // Write the remainder after the last double-quote. + Write(literal, start, literal.Length - start); + + Write("\""); + } + + private void WriteCStyleStringLiteral(string literal) + { + // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM + Write("\""); + + // We need to find the index of each escapable character to escape it. + var start = 0; + int end; + while ((end = literal.IndexOfAny(CStyleStringLiteralEscapeChars, start)) > -1) + { + Write(literal, start, end - start); + + switch (literal[end]) + { + case '\r': + Write("\\r"); + break; + case '\t': + Write("\\t"); + break; + case '\"': + Write("\\\""); + break; + case '\'': + Write("\\\'"); + break; + case '\\': + Write("\\\\"); + break; + case '\0': + Write("\\\0"); + break; + case '\n': + Write("\\n"); + break; + case '\u2028': + case '\u2029': + Write("\\u"); + Write(((int)literal[end]).ToString("X4", CultureInfo.InvariantCulture)); + break; + default: + Debug.Assert(false, "Unknown escape character."); + break; + } + + start = end + 1; + } + + Debug.Assert(end == -1); // We've hit all of chars that need escaping. + + // Write the remainder after the last escaped char. + Write(literal, start, literal.Length - start); + + Write("\""); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWritingScope.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWritingScope.cs new file mode 100644 index 0000000000..dbdc7c0f90 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWritingScope.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal struct CSharpCodeWritingScope : IDisposable + { + private CodeWriter _writer; + private bool _autoSpace; + private int _tabSize; + private int _startIndent; + + public CSharpCodeWritingScope(CodeWriter writer) : this(writer, true) { } + public CSharpCodeWritingScope(CodeWriter writer, int tabSize) : this(writer, tabSize, true) { } + // TODO: Make indents (tabs) environment specific + public CSharpCodeWritingScope(CodeWriter writer, bool autoSpace) : this(writer, 4, autoSpace) { } + public CSharpCodeWritingScope(CodeWriter writer, int tabSize, bool autoSpace) + { + _writer = writer; + _autoSpace = true; + _tabSize = tabSize; + _startIndent = -1; // Set in WriteStartScope + + OnClose = () => { }; + + WriteStartScope(); + } + + public Action OnClose; + + public void Dispose() + { + WriteEndScope(); + OnClose(); + } + + private void WriteStartScope() + { + TryAutoSpace(" "); + + _writer.WriteLine("{").IncreaseIndent(_tabSize); + _startIndent = _writer.CurrentIndent; + } + + private void WriteEndScope() + { + TryAutoSpace(_writer.NewLine); + + // Ensure the scope hasn't been modified + if (_writer.CurrentIndent == _startIndent) + { + _writer.DecreaseIndent(_tabSize); + } + + _writer.WriteLine("}"); + } + + private void TryAutoSpace(string spaceCharacter) + { + if (_autoSpace && + _writer.Builder.Length > 0 && + !char.IsWhiteSpace(_writer.Builder[_writer.Builder.Length - 1])) + { + _writer.Write(spaceCharacter); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpDisableWarningScope.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpDisableWarningScope.cs new file mode 100644 index 0000000000..00ce448b1a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpDisableWarningScope.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal struct CSharpDisableWarningScope : IDisposable + { + private CSharpCodeWriter _writer; + int _warningNumber; + + public CSharpDisableWarningScope(CSharpCodeWriter writer) : this(writer, 219) + { } + + public CSharpDisableWarningScope(CSharpCodeWriter writer, int warningNumber) + { + _writer = writer; + _warningNumber = warningNumber; + + _writer.WritePragma("warning disable " + _warningNumber); + } + + public void Dispose() + { + _writer.WritePragma("warning restore " + _warningNumber); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CodeWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CodeWriter.cs new file mode 100644 index 0000000000..e37034f0a5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CodeWriter.cs @@ -0,0 +1,203 @@ +// Copyright (c) .NET Foundation. 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.Text; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CodeWriter : IDisposable + { + private static readonly char[] NewLineCharacters = { '\r', '\n' }; + + private string _cache = string.Empty; + private bool _dirty; + + private int _absoluteIndex; + private int _currentLineIndex; + private int _currentLineCharacterIndex; + + public StringBuilder Builder { get; } = new StringBuilder(); + + public int CurrentIndent { get; private set; } + + public bool IsAfterNewLine { get; private set; } + + public string NewLine { get; set; } = Environment.NewLine; + + public CodeWriter ResetIndent() + { + return SetIndent(0); + } + + public CodeWriter IncreaseIndent(int size) + { + CurrentIndent += size; + + return this; + } + + public CodeWriter DecreaseIndent(int size) + { + CurrentIndent -= size; + + return this; + } + + public CodeWriter SetIndent(int size) + { + CurrentIndent = size; + + return this; + } + + public CodeWriter Indent(int size) + { + if (IsAfterNewLine) + { + Builder.Append(' ', size); + + _currentLineCharacterIndex += size; + _absoluteIndex += size; + + _dirty = true; + IsAfterNewLine = false; + } + + return this; + } + + public CodeWriter Write(string data) + { + if (data == null) + { + return this; + } + + return Write(data, 0, data.Length); + } + + public CodeWriter Write(string data, int index, int count) + { + if (data == null || count == 0) + { + return this; + } + + Indent(CurrentIndent); + + Builder.Append(data, index, count); + + _dirty = true; + IsAfterNewLine = false; + + _absoluteIndex += count; + + // The data string might contain a partial newline where the previously + // written string has part of the newline. + var i = index; + int? trailingPartStart = null; + + if ( + // Check the last character of the previous write operation. + Builder.Length - count - 1 >= 0 && + Builder[Builder.Length - count - 1] == '\r' && + + // Check the first character of the current write operation. + Builder[Builder.Length - count] == '\n') + { + // This is newline that's spread across two writes. Skip the first character of the + // current write operation. + // + // We don't need to increment our newline counter because we already did that when we + // saw the \r. + i += 1; + trailingPartStart = 1; + } + + // Iterate the string, stopping at each occurrence of a newline character. This lets us count the + // newline occurrences and keep the index of the last one. + while ((i = data.IndexOfAny(NewLineCharacters, i)) >= 0) + { + // Newline found. + _currentLineIndex++; + _currentLineCharacterIndex = 0; + + i++; + + // We might have stopped at a \r, so check if it's followed by \n and then advance the index to + // start the next search after it. + if (count > i && + data[i - 1] == '\r' && + data[i] == '\n') + { + i++; + } + + // The 'suffix' of the current line starts after this newline token. + trailingPartStart = i; + } + + if (trailingPartStart == null) + { + // No newlines, just add the length of the data buffer + _currentLineCharacterIndex += count; + } + else + { + // Newlines found, add the trailing part of 'data' + _currentLineCharacterIndex += (count - trailingPartStart.Value); + } + + return this; + } + + public CodeWriter WriteLine() + { + Builder.Append(NewLine); + + _currentLineIndex++; + _currentLineCharacterIndex = 0; + _absoluteIndex += NewLine.Length; + + _dirty = true; + IsAfterNewLine = true; + + return this; + } + + public CodeWriter WriteLine(string data) + { + return Write(data).WriteLine(); + } + + public string GenerateCode() + { + if (_dirty) + { + _cache = Builder.ToString(); + _dirty = false; + } + + return _cache; + } + + public SourceLocation GetCurrentSourceLocation() + { + return new SourceLocation(_absoluteIndex, _currentLineIndex, _currentLineCharacterIndex); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Builder.Clear(); + } + } + + public void Dispose() + { + Dispose(disposing: true); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMapping.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMapping.cs new file mode 100644 index 0000000000..3042010f9a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMapping.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class LineMapping + { + public LineMapping(MappingLocation documentLocation, MappingLocation generatedLocation) + { + DocumentLocation = documentLocation; + GeneratedLocation = generatedLocation; + } + + public MappingLocation DocumentLocation { get; } + + public MappingLocation GeneratedLocation { get; } + + public override bool Equals(object obj) + { + var other = obj as LineMapping; + if (ReferenceEquals(other, null)) + { + return false; + } + + return DocumentLocation.Equals(other.DocumentLocation) && + GeneratedLocation.Equals(other.GeneratedLocation); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(DocumentLocation); + hashCodeCombiner.Add(GeneratedLocation); + + return hashCodeCombiner; + } + + public static bool operator ==(LineMapping left, LineMapping right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return true; + } + + if (ReferenceEquals(left, null)) + { + return false; + } + + return left.Equals(right); + } + + public static bool operator !=(LineMapping left, LineMapping right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return false; + } + + if (ReferenceEquals(left, null)) + { + return true; + } + + return !left.Equals(right); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "{0} -> {1}", DocumentLocation, GeneratedLocation); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMappingManager.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMappingManager.cs new file mode 100644 index 0000000000..7302ed738a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/LineMappingManager.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class LineMappingManager + { + public LineMappingManager() + { + Mappings = new List(); + } + + public List Mappings { get; } + + public void AddMapping(MappingLocation documentLocation, MappingLocation generatedLocation) + { + Mappings.Add(new LineMapping(documentLocation, generatedLocation)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/MappingLocation.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/MappingLocation.cs new file mode 100644 index 0000000000..1ce02080b4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/MappingLocation.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. 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.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class MappingLocation + { + public MappingLocation() + { + } + + public MappingLocation(SourceLocation location, int contentLength) + { + ContentLength = contentLength; + AbsoluteIndex = location.AbsoluteIndex; + LineIndex = location.LineIndex; + CharacterIndex = location.CharacterIndex; + FilePath = location.FilePath; + } + + public int ContentLength { get; } + + public int AbsoluteIndex { get; } + + public int LineIndex { get; } + + public int CharacterIndex { get; } + + public string FilePath { get; } + + public override bool Equals(object obj) + { + var other = obj as MappingLocation; + if (ReferenceEquals(other, null)) + { + return false; + } + + return string.Equals(FilePath, other.FilePath, StringComparison.Ordinal) && + AbsoluteIndex == other.AbsoluteIndex && + ContentLength == other.ContentLength && + LineIndex == other.LineIndex && + CharacterIndex == other.CharacterIndex; + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(FilePath, StringComparer.Ordinal); + hashCodeCombiner.Add(AbsoluteIndex); + hashCodeCombiner.Add(ContentLength); + hashCodeCombiner.Add(LineIndex); + hashCodeCombiner.Add(CharacterIndex); + + return hashCodeCombiner; + } + + public override string ToString() + { + return string.Format( + CultureInfo.CurrentCulture, "({0}:{1},{2} [{3}] {4})", + AbsoluteIndex, + LineIndex, + CharacterIndex, + ContentLength, + FilePath); + } + + public static bool operator ==(MappingLocation left, MappingLocation right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return true; + } + + if (ReferenceEquals(left, null)) + { + return false; + } + + return left.Equals(right); + } + + public static bool operator !=(MappingLocation left, MappingLocation right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return false; + } + + if (ReferenceEquals(left, null)) + { + return true; + } + + return !left.Equals(right); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs new file mode 100644 index 0000000000..f9daf97f1f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class CSharpCodeWriterTest + { + [Fact] + public void WriteLineNumberDirective_UsesFilePath_WhenFileInSourceLocationIsNull() + { + // Arrange + var filePath = "some-path"; + var writer = new CSharpCodeWriter(); + var expected = $"#line 5 \"{filePath}\"" + writer.NewLine; + var sourceLocation = new SourceLocation(10, 4, 3); + + // Act + writer.WriteLineNumberDirective(sourceLocation, filePath); + var code = writer.GenerateCode(); + + // Assert + Assert.Equal(expected, code); + } + + [Theory] + [InlineData("")] + [InlineData("source-location-file-path")] + public void WriteLineNumberDirective_UsesSourceLocationFilePath_IfAvailable( + string sourceLocationFilePath) + { + // Arrange + var filePath = "some-path"; + var writer = new CSharpCodeWriter(); + var expected = $"#line 5 \"{sourceLocationFilePath}\"" + writer.NewLine; + var sourceLocation = new SourceLocation(sourceLocationFilePath, 10, 4, 3); + + // Act + writer.WriteLineNumberDirective(sourceLocation, filePath); + var code = writer.GenerateCode(); + + // Assert + Assert.Equal(expected, code); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeWriterTest.cs new file mode 100644 index 0000000000..b6e7479a3e --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeWriterTest.cs @@ -0,0 +1,265 @@ +// Copyright (c) .NET Foundation. 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class CodeWriterTest + { + // The length of the newline string written by writer.WriteLine. + private static readonly int WriterNewLineLength = Environment.NewLine.Length; + + public static IEnumerable NewLines + { + get + { + return new object[][] + { + new object[] { "\r" }, + new object[] { "\n" }, + new object[] { "\r\n" }, + }; + } + } + + [Fact] + public void CodeWriter_TracksPosition_WithWrite() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234"); + + // Assert + var location = writer.GetCurrentSourceLocation(); + var expected = new SourceLocation(absoluteIndex: 4, lineIndex: 0, characterIndex: 4); + + Assert.Equal(expected, location); + } + + [Fact] + public void CodeWriter_TracksPosition_WithIndent() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.WriteLine(); + writer.Indent(size: 3); + + // Assert + var location = writer.GetCurrentSourceLocation(); + var expected = new SourceLocation(absoluteIndex: 3 + WriterNewLineLength, lineIndex: 1, characterIndex: 3); + + Assert.Equal(expected, location); + } + + [Fact] + public void CodeWriter_TracksPosition_WithWriteLine() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.WriteLine("1234"); + + // Assert + var location = writer.GetCurrentSourceLocation(); + + var expected = new SourceLocation(absoluteIndex: 4 + WriterNewLineLength, lineIndex: 1, characterIndex: 0); + + Assert.Equal(expected, location); + } + + [Theory] + [MemberData("NewLines")] + public void CodeWriter_TracksPosition_WithWriteLine_WithNewLineInContent(string newLine) + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.WriteLine("1234" + newLine + "12"); + + // Assert + var location = writer.GetCurrentSourceLocation(); + + var expected = new SourceLocation( + absoluteIndex: 6 + newLine.Length + WriterNewLineLength, + lineIndex: 2, + characterIndex: 0); + + Assert.Equal(expected, location); + } + + [Theory] + [MemberData("NewLines")] + public void CodeWriter_TracksPosition_WithWrite_WithNewlineInContent(string newLine) + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234" + newLine + "123" + newLine + "12"); + + // Assert + var location = writer.GetCurrentSourceLocation(); + + var expected = new SourceLocation( + absoluteIndex: 9 + newLine.Length + newLine.Length, + lineIndex: 2, + characterIndex: 2); + + Assert.Equal(expected, location); + } + + [Fact] + public void CodeWriter_TracksPosition_WithWrite_WithNewlineInContent_RepeatedN() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234\n\n123"); + + // Assert + var location = writer.GetCurrentSourceLocation(); + + var expected = new SourceLocation( + absoluteIndex: 9, + lineIndex: 2, + characterIndex: 3); + + Assert.Equal(expected, location); + } + + [Fact] + public void CodeWriter_TracksPosition_WithWrite_WithMixedNewlineInContent() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234\r123\r\n12\n1"); + + // Assert + var location = writer.GetCurrentSourceLocation(); + + var expected = new SourceLocation( + absoluteIndex: 14, + lineIndex: 3, + characterIndex: 1); + + Assert.Equal(expected, location); + } + + [Fact] + public void CodeWriter_TracksPosition_WithNewline_SplitAcrossWrites() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234\r"); + var location1 = writer.GetCurrentSourceLocation(); + + writer.Write("\n"); + var location2 = writer.GetCurrentSourceLocation(); + + // Assert + var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected1, location1); + + var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected2, location2); + } + + [Fact] + public void CodeWriter_TracksPosition_WithTwoNewline_SplitAcrossWrites_R() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234\r"); + var location1 = writer.GetCurrentSourceLocation(); + + writer.Write("\r"); + var location2 = writer.GetCurrentSourceLocation(); + + // Assert + var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected1, location1); + + var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 2, characterIndex: 0); + Assert.Equal(expected2, location2); + } + + [Fact] + public void CodeWriter_TracksPosition_WithTwoNewline_SplitAcrossWrites_N() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234\n"); + var location1 = writer.GetCurrentSourceLocation(); + + writer.Write("\n"); + var location2 = writer.GetCurrentSourceLocation(); + + // Assert + var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected1, location1); + + var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 2, characterIndex: 0); + Assert.Equal(expected2, location2); + } + + [Fact] + public void CodeWriter_TracksPosition_WithTwoNewline_SplitAcrossWrites_Reversed() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("1234\n"); + var location1 = writer.GetCurrentSourceLocation(); + + writer.Write("\r"); + var location2 = writer.GetCurrentSourceLocation(); + + // Assert + var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected1, location1); + + var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 2, characterIndex: 0); + Assert.Equal(expected2, location2); + } + + [Fact] + public void CodeWriter_TracksPosition_WithNewline_SplitAcrossWrites_AtBeginning() + { + // Arrange + var writer = new CodeWriter(); + + // Act + writer.Write("\r"); + var location1 = writer.GetCurrentSourceLocation(); + + writer.Write("\n"); + var location2 = writer.GetCurrentSourceLocation(); + + // Assert + var expected1 = new SourceLocation(absoluteIndex: 1, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected1, location1); + + var expected2 = new SourceLocation(absoluteIndex: 2, lineIndex: 1, characterIndex: 0); + Assert.Equal(expected2, location2); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineMappingTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineMappingTest.cs new file mode 100644 index 0000000000..e293e6064b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineMappingTest.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class LineMappingTest + { + [Fact] + public void GeneratedCodeMappingsAreEqualIfDataIsEqual() + { + // Arrange + var left = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 8) + ); + var right = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 8) + ); + + // Assert + Assert.True(left == right); + Assert.True(left.Equals(right)); + Assert.True(right.Equals(left)); + Assert.True(Equals(left, right)); + } + + [Fact] + public void GeneratedCodeMappingsAreNotEqualIfCodeLengthIsNotEqual() + { + // Arrange + var left = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 8) + ); + var right = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 5), + new MappingLocation(new SourceLocation(5, 6, 7), 9) + ); + + // Assert + AssertNotEqual(left, right); + } + + [Fact] + public void GeneratedCodeMappingsAreNotEqualIfStartGeneratedColumnIsNotEqual() + { + // Arrange + var left = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 8) + ); + var right = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 8), 8) + ); + + // Assert + AssertNotEqual(left, right); + } + + [Fact] + public void GeneratedCodeMappingsAreNotEqualIfStartColumnIsNotEqual() + { + // Arrange + var left = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 8), 8) + ); + var right = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 8) + ); + + // Assert + AssertNotEqual(left, right); + } + + [Fact] + public void GeneratedCodeMappingsAreNotEqualIfStartLineIsNotEqual() + { + // Arrange + var left = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 5, 7), 8) + ); + var right = new LineMapping( + new MappingLocation(new SourceLocation(1, 1, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 8) + ); + + // Assert + AssertNotEqual(left, right); + } + + [Fact] + public void GeneratedCodeMappingsAreNotEqualIfAbsoluteIndexIsNotEqual() + { + // Arrange + var left = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(4, 6, 7), 8) + ); + var right = new LineMapping( + new MappingLocation(new SourceLocation(1, 2, 3), 4), + new MappingLocation(new SourceLocation(5, 6, 7), 9) + ); + + // Assert + AssertNotEqual(left, right); + } + + private void AssertNotEqual(LineMapping left, LineMapping right) + { + Assert.False(left == right); + Assert.False(left.Equals(right)); + Assert.False(right.Equals(left)); + Assert.False(Equals(left, right)); + } + } +} \ No newline at end of file