787 lines
23 KiB
C#
787 lines
23 KiB
C#
// 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;
|
|
using System.Text;
|
|
|
|
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
|
|
{
|
|
public sealed class CodeWriter
|
|
{
|
|
private const string InstanceMethodFormat = "{0}.{1}";
|
|
|
|
private static readonly char[] CStyleStringLiteralEscapeChars = {
|
|
'\r',
|
|
'\t',
|
|
'\"',
|
|
'\'',
|
|
'\\',
|
|
'\0',
|
|
'\n',
|
|
'\u2028',
|
|
'\u2029',
|
|
};
|
|
|
|
private static readonly char[] NewLineCharacters = { '\r', '\n' };
|
|
|
|
private string _cache = string.Empty;
|
|
private bool _dirty;
|
|
|
|
private int _absoluteIndex;
|
|
private int _currentLineIndex;
|
|
private int _currentLineCharacterIndex;
|
|
|
|
internal StringBuilder Builder { get; } = new StringBuilder();
|
|
|
|
public int CurrentIndent { get; set; }
|
|
|
|
public bool IsAfterNewLine { get; private set; }
|
|
|
|
public string NewLine { get; set; } = Environment.NewLine;
|
|
|
|
public SourceLocation Location => new SourceLocation(_absoluteIndex, _currentLineIndex, _currentLineCharacterIndex);
|
|
|
|
// Internal for testing.
|
|
internal 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 CodeWriter WritePadding(int offset, SourceSpan? span, CodeRenderingContext context)
|
|
{
|
|
if (span == null)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
var basePadding = CalculatePadding();
|
|
var resolvedPadding = Math.Max(basePadding - offset, 0);
|
|
|
|
if (context.Options.IndentWithTabs)
|
|
{
|
|
// Avoid writing directly to the StringBuilder here, that will throw off the manual indexing
|
|
// done by the base class.
|
|
var tabs = resolvedPadding / context.Options.IndentSize;
|
|
for (var i = 0; i < tabs; i++)
|
|
{
|
|
Write("\t");
|
|
}
|
|
|
|
var spaces = resolvedPadding % context.Options.IndentSize;
|
|
for (var i = 0; i < spaces; i++)
|
|
{
|
|
Write(" ");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < resolvedPadding; i++)
|
|
{
|
|
Write(" ");
|
|
}
|
|
}
|
|
|
|
return this;
|
|
|
|
int CalculatePadding()
|
|
{
|
|
var spaceCount = 0;
|
|
for (var i = span.Value.AbsoluteIndex - 1; i >= 0; i--)
|
|
{
|
|
var @char = context.SourceDocument[i];
|
|
if (@char == '\n' || @char == '\r')
|
|
{
|
|
break;
|
|
}
|
|
else if (@char == '\t')
|
|
{
|
|
spaceCount += context.Options.IndentSize;
|
|
}
|
|
else
|
|
{
|
|
spaceCount++;
|
|
}
|
|
}
|
|
|
|
return spaceCount;
|
|
}
|
|
}
|
|
|
|
public CodeWriter 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 CodeWriter WriteBooleanLiteral(bool value)
|
|
{
|
|
return Write(value.ToString().ToLowerInvariant());
|
|
}
|
|
|
|
public CodeWriter WriteStartAssignment(string name)
|
|
{
|
|
return Write(name).Write(" = ");
|
|
}
|
|
|
|
public CodeWriter WriteParameterSeparator()
|
|
{
|
|
return Write(", ");
|
|
}
|
|
|
|
public CodeWriter WriteStartNewObject(string typeName)
|
|
{
|
|
return Write("new ").Write(typeName).Write("(");
|
|
}
|
|
|
|
public CodeWriter WriteStringLiteral(string literal)
|
|
{
|
|
if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1)
|
|
{
|
|
WriteVerbatimStringLiteral(literal);
|
|
}
|
|
else
|
|
{
|
|
WriteCStyleStringLiteral(literal);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public CodeWriter WriteUsing(string name)
|
|
{
|
|
return WriteUsing(name, endLine: true);
|
|
}
|
|
|
|
public CodeWriter WriteUsing(string name, bool endLine)
|
|
{
|
|
Write("using ");
|
|
Write(name);
|
|
|
|
if (endLine)
|
|
{
|
|
WriteLine(";");
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a <c>#line</c> pragma directive for the line number at the specified <paramref name="location"/>.
|
|
/// </summary>
|
|
/// <param name="location">The location to generate the line pragma for.</param>
|
|
/// <param name="file">The file to generate the line pragma for.</param>
|
|
/// <returns>The current instance of <see cref="CodeWriter"/>.</returns>
|
|
public CodeWriter WriteLineNumberDirective(SourceSpan 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 CodeWriter WriteStartMethodInvocation(string methodName)
|
|
{
|
|
Write(methodName);
|
|
|
|
return Write("(");
|
|
}
|
|
|
|
public CodeWriter WriteEndMethodInvocation()
|
|
{
|
|
return WriteEndMethodInvocation(endLine: true);
|
|
}
|
|
|
|
public CodeWriter WriteEndMethodInvocation(bool endLine)
|
|
{
|
|
Write(")");
|
|
if (endLine)
|
|
{
|
|
WriteLine(";");
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
// Writes a method invocation for the given instance name.
|
|
public CodeWriter 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 CodeWriter 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 CodeWriter 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 CodeWriter WriteField(IList<string> modifiers, string typeName, string fieldName)
|
|
{
|
|
if (modifiers == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(modifiers));
|
|
}
|
|
|
|
if (typeName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(typeName));
|
|
}
|
|
|
|
if (fieldName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(fieldName));
|
|
}
|
|
|
|
for (var i = 0; i < modifiers.Count; i++)
|
|
{
|
|
Write(modifiers[i]);
|
|
Write(" ");
|
|
}
|
|
|
|
Write(typeName);
|
|
Write(" ");
|
|
Write(fieldName);
|
|
Write(";");
|
|
WriteLine();
|
|
|
|
return this;
|
|
}
|
|
|
|
public CodeWriter WriteMethodInvocation(string methodName, params string[] parameters)
|
|
{
|
|
return WriteMethodInvocation(methodName, endLine: true, parameters: parameters);
|
|
}
|
|
|
|
public CodeWriter WriteMethodInvocation(string methodName, bool endLine, params string[] parameters)
|
|
{
|
|
return WriteStartMethodInvocation(methodName)
|
|
.Write(string.Join(", ", parameters))
|
|
.WriteEndMethodInvocation(endLine);
|
|
}
|
|
|
|
public CodeWriter WriteAutoPropertyDeclaration(IList<string> modifiers, string typeName, string propertyName)
|
|
{
|
|
if (modifiers == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(modifiers));
|
|
}
|
|
|
|
if (typeName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(typeName));
|
|
}
|
|
|
|
if (propertyName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(propertyName));
|
|
}
|
|
|
|
for (var i = 0; i < modifiers.Count; i++)
|
|
{
|
|
Write(modifiers[i]);
|
|
Write(" ");
|
|
}
|
|
|
|
Write(typeName);
|
|
Write(" ");
|
|
Write(propertyName);
|
|
Write(" { get; set; }");
|
|
WriteLine();
|
|
|
|
return this;
|
|
}
|
|
|
|
public CSharpCodeWritingScope BuildScope()
|
|
{
|
|
return new CSharpCodeWritingScope(this);
|
|
}
|
|
|
|
public CSharpCodeWritingScope BuildLambda(params string[] parameterNames)
|
|
{
|
|
return BuildLambda(async: false, parameterNames: parameterNames);
|
|
}
|
|
|
|
public CSharpCodeWritingScope BuildAsyncLambda(params string[] parameterNames)
|
|
{
|
|
return BuildLambda(async: true, parameterNames: parameterNames);
|
|
}
|
|
|
|
private CSharpCodeWritingScope BuildLambda(bool async, string[] parameterNames)
|
|
{
|
|
if (async)
|
|
{
|
|
Write("async");
|
|
}
|
|
|
|
Write("(").Write(string.Join(", ", parameterNames)).Write(") => ");
|
|
|
|
var scope = new CSharpCodeWritingScope(this);
|
|
|
|
return scope;
|
|
}
|
|
|
|
public CSharpCodeWritingScope BuildNamespace(string name)
|
|
{
|
|
Write("namespace ").WriteLine(name);
|
|
|
|
return new CSharpCodeWritingScope(this);
|
|
}
|
|
|
|
public CSharpCodeWritingScope BuildClassDeclaration(
|
|
IList<string> modifiers,
|
|
string name,
|
|
string baseType,
|
|
IEnumerable<string> interfaces)
|
|
{
|
|
for (var i = 0; i < modifiers.Count; i++)
|
|
{
|
|
Write(modifiers[i]);
|
|
Write(" ");
|
|
}
|
|
|
|
Write("class ");
|
|
Write(name);
|
|
|
|
var hasBaseType = !string.IsNullOrEmpty(baseType);
|
|
var hasInterfaces = interfaces != null && interfaces.Count() > 0;
|
|
|
|
if (hasBaseType || hasInterfaces)
|
|
{
|
|
Write(" : ");
|
|
|
|
if (hasBaseType)
|
|
{
|
|
Write(baseType);
|
|
|
|
if (hasInterfaces)
|
|
{
|
|
WriteParameterSeparator();
|
|
}
|
|
}
|
|
|
|
if (hasInterfaces)
|
|
{
|
|
Write(string.Join(", ", interfaces));
|
|
}
|
|
}
|
|
|
|
WriteLine();
|
|
|
|
return new CSharpCodeWritingScope(this);
|
|
}
|
|
|
|
public CSharpCodeWritingScope BuildMethodDeclaration(
|
|
string accessibility,
|
|
string returnType,
|
|
string name,
|
|
IEnumerable<KeyValuePair<string, string>> 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);
|
|
}
|
|
|
|
public IDisposable BuildLinePragma(SourceSpan? span)
|
|
{
|
|
if (string.IsNullOrEmpty(span?.FilePath))
|
|
{
|
|
// Can't build a valid line pragma without a file path.
|
|
return NullDisposable.Default;
|
|
}
|
|
|
|
return new LinePragmaWriter(this, span.Value);
|
|
}
|
|
|
|
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("\"");
|
|
}
|
|
|
|
public struct CSharpCodeWritingScope : IDisposable
|
|
{
|
|
private CodeWriter _writer;
|
|
private bool _autoSpace;
|
|
private int _tabSize;
|
|
private int _startIndent;
|
|
|
|
public CSharpCodeWritingScope(CodeWriter writer, int tabSize = 4, bool autoSpace = true)
|
|
{
|
|
_writer = writer;
|
|
_autoSpace = true;
|
|
_tabSize = tabSize;
|
|
_startIndent = -1; // Set in WriteStartScope
|
|
|
|
WriteStartScope();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
WriteEndScope();
|
|
}
|
|
|
|
private void WriteStartScope()
|
|
{
|
|
TryAutoSpace(" ");
|
|
|
|
_writer.WriteLine("{");
|
|
_writer.CurrentIndent += _tabSize;
|
|
_startIndent = _writer.CurrentIndent;
|
|
}
|
|
|
|
private void WriteEndScope()
|
|
{
|
|
TryAutoSpace(_writer.NewLine);
|
|
|
|
// Ensure the scope hasn't been modified
|
|
if (_writer.CurrentIndent == _startIndent)
|
|
{
|
|
_writer.CurrentIndent -= _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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class LinePragmaWriter : IDisposable
|
|
{
|
|
private readonly CodeWriter _writer;
|
|
private readonly int _startIndent;
|
|
|
|
public LinePragmaWriter(CodeWriter writer, SourceSpan documentLocation)
|
|
{
|
|
if (writer == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(writer));
|
|
}
|
|
|
|
_writer = writer;
|
|
_startIndent = _writer.CurrentIndent;
|
|
_writer.CurrentIndent = 0;
|
|
_writer.WriteLineNumberDirective(documentLocation, documentLocation.FilePath);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Need to add an additional line at the end IF there wasn't one already written.
|
|
// This is needed to work with the C# editor's handling of #line ...
|
|
var builder = _writer.Builder;
|
|
var endsWithNewline = builder.Length > 0 && builder[builder.Length - 1] == '\n';
|
|
|
|
// Always write at least 1 empty line to potentially separate code from pragmas.
|
|
_writer.WriteLine();
|
|
|
|
// Check if the previous empty line wasn't enough to separate code from pragmas.
|
|
if (!endsWithNewline)
|
|
{
|
|
_writer.WriteLine();
|
|
}
|
|
|
|
_writer
|
|
.WriteLine("#line default")
|
|
.WriteLine("#line hidden");
|
|
|
|
_writer.CurrentIndent = _startIndent;
|
|
}
|
|
}
|
|
|
|
private class NullDisposable : IDisposable
|
|
{
|
|
public static readonly NullDisposable Default = new NullDisposable();
|
|
|
|
private NullDisposable()
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|