Files
runner/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateReader.cs
2020-03-18 12:08:51 -04:00

825 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using GitHub.DistributedTask.Expressions2.Sdk;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.ObjectTemplating.Schema;
namespace GitHub.DistributedTask.ObjectTemplating
{
/// <summary>
/// Converts a source object format into a TemplateToken
/// </summary>
internal sealed class TemplateReader
{
private TemplateReader(
TemplateContext context,
TemplateSchema schema,
IObjectReader objectReader,
Int32? fileId)
{
m_context = context;
m_schema = schema;
m_memory = context.Memory;
m_objectReader = objectReader;
m_fileId = fileId;
}
internal static TemplateToken Read(
TemplateContext context,
String type,
IObjectReader objectReader,
Int32? fileId,
out Int32 bytes)
{
return Read(context, type, objectReader, fileId, context.Schema, out bytes);
}
internal static TemplateToken Read(
TemplateContext context,
String type,
IObjectReader objectReader,
Int32? fileId,
TemplateSchema schema,
out Int32 bytes)
{
TemplateToken result = null;
var reader = new TemplateReader(context, schema, objectReader, fileId);
var originalBytes = context.Memory.CurrentBytes;
try
{
objectReader.ValidateStart();
var definition = new DefinitionInfo(schema, type);
result = reader.ReadValue(definition);
objectReader.ValidateEnd();
}
catch (Exception ex)
{
context.Error(fileId, null, null, ex);
}
finally
{
bytes = context.Memory.CurrentBytes - originalBytes;
}
return result;
}
private TemplateToken ReadValue(DefinitionInfo definition)
{
m_memory.IncrementEvents();
// Scalar
if (m_objectReader.AllowLiteral(out LiteralToken literal))
{
var scalar = ParseScalar(literal, definition.AllowedContext);
Validate(ref scalar, definition);
m_memory.AddBytes(scalar);
return scalar;
}
// Sequence
if (m_objectReader.AllowSequenceStart(out SequenceToken sequence))
{
m_memory.IncrementDepth();
m_memory.AddBytes(sequence);
var sequenceDefinition = definition.Get<SequenceDefinition>().FirstOrDefault();
// Legal
if (sequenceDefinition != null)
{
var itemDefinition = new DefinitionInfo(definition, sequenceDefinition.ItemType);
// Add each item
while (!m_objectReader.AllowSequenceEnd())
{
var item = ReadValue(itemDefinition);
sequence.Add(item);
}
}
// Illegal
else
{
// Error
m_context.Error(sequence, TemplateStrings.UnexpectedSequenceStart());
// Skip each item
while (!m_objectReader.AllowSequenceEnd())
{
SkipValue();
}
}
m_memory.DecrementDepth();
return sequence;
}
// Mapping
if (m_objectReader.AllowMappingStart(out MappingToken mapping))
{
m_memory.IncrementDepth();
m_memory.AddBytes(mapping);
var mappingDefinitions = definition.Get<MappingDefinition>().ToList();
// Legal
if (mappingDefinitions.Count > 0)
{
if (mappingDefinitions.Count > 1 ||
m_schema.HasProperties(mappingDefinitions[0]) ||
String.IsNullOrEmpty(mappingDefinitions[0].LooseKeyType))
{
HandleMappingWithWellKnownProperties(definition, mappingDefinitions, mapping);
}
else
{
var keyDefinition = new DefinitionInfo(definition, mappingDefinitions[0].LooseKeyType);
var valueDefinition = new DefinitionInfo(definition, mappingDefinitions[0].LooseValueType);
HandleMappingWithAllLooseProperties(definition, keyDefinition, valueDefinition, mapping);
}
}
// Illegal
else
{
m_context.Error(mapping, TemplateStrings.UnexpectedMappingStart());
while (!m_objectReader.AllowMappingEnd())
{
SkipValue();
SkipValue();
}
}
m_memory.DecrementDepth();
return mapping;
}
throw new InvalidOperationException(TemplateStrings.ExpectedScalarSequenceOrMapping());
}
private void HandleMappingWithWellKnownProperties(
DefinitionInfo definition,
List<MappingDefinition> mappingDefinitions,
MappingToken mapping)
{
// Check if loose properties are allowed
String looseKeyType = null;
String looseValueType = null;
DefinitionInfo? looseKeyDefinition = null;
DefinitionInfo? looseValueDefinition = null;
if (!String.IsNullOrEmpty(mappingDefinitions[0].LooseKeyType))
{
looseKeyType = mappingDefinitions[0].LooseKeyType;
looseValueType = mappingDefinitions[0].LooseValueType;
}
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
var hasExpressionKey = false;
while (m_objectReader.AllowLiteral(out LiteralToken rawLiteral))
{
var nextKeyScalar = ParseScalar(rawLiteral, definition.AllowedContext);
// Expression
if (nextKeyScalar is ExpressionToken)
{
hasExpressionKey = true;
// Legal
if (definition.AllowedContext.Length > 0)
{
m_memory.AddBytes(nextKeyScalar);
var anyDefinition = new DefinitionInfo(definition, TemplateConstants.Any);
mapping.Add(nextKeyScalar, ReadValue(anyDefinition));
}
// Illegal
else
{
m_context.Error(nextKeyScalar, TemplateStrings.ExpressionNotAllowed());
SkipValue();
}
continue;
}
// Not a string, convert
if (!(nextKeyScalar is StringToken nextKey))
{
nextKey = new StringToken(nextKeyScalar.FileId, nextKeyScalar.Line, nextKeyScalar.Column, nextKeyScalar.ToString());
}
// Duplicate
if (!keys.Add(nextKey.Value))
{
m_context.Error(nextKey, TemplateStrings.ValueAlreadyDefined(nextKey.Value));
SkipValue();
continue;
}
// Well known
if (m_schema.TryMatchKey(mappingDefinitions, nextKey.Value, out String nextValueType))
{
m_memory.AddBytes(nextKey);
var nextValueDefinition = new DefinitionInfo(definition, nextValueType);
var nextValue = ReadValue(nextValueDefinition);
mapping.Add(nextKey, nextValue);
continue;
}
// Loose
if (looseKeyType != null)
{
if (looseKeyDefinition == null)
{
looseKeyDefinition = new DefinitionInfo(definition, looseKeyType);
looseValueDefinition = new DefinitionInfo(definition, looseValueType);
}
Validate(nextKey, looseKeyDefinition.Value);
m_memory.AddBytes(nextKey);
var nextValue = ReadValue(looseValueDefinition.Value);
mapping.Add(nextKey, nextValue);
continue;
}
// Error
m_context.Error(nextKey, TemplateStrings.UnexpectedValue(nextKey.Value));
SkipValue();
}
// Only one
if (mappingDefinitions.Count > 1)
{
var hitCount = new Dictionary<String, Int32>();
foreach (MappingDefinition mapdef in mappingDefinitions)
{
foreach (String key in mapdef.Properties.Keys)
{
if (!hitCount.TryGetValue(key, out Int32 value))
{
hitCount.Add(key, 1);
}
else
{
hitCount[key] = value + 1;
}
}
}
List<String> nonDuplicates = new List<String>();
foreach (String key in hitCount.Keys)
{
if(hitCount[key] == 1)
{
nonDuplicates.Add(key);
}
}
nonDuplicates.Sort();
String listToDeDuplicate = String.Join(", ", nonDuplicates);
m_context.Error(mapping, TemplateStrings.UnableToDetermineOneOf(listToDeDuplicate));
}
else if (mappingDefinitions.Count == 1 && !hasExpressionKey)
{
foreach (var property in mappingDefinitions[0].Properties)
{
if (property.Value.Required)
{
if (!keys.Contains(property.Key))
{
m_context.Error(mapping, $"Required property is missing: {property.Key}");
}
}
}
}
ExpectMappingEnd();
}
private void HandleMappingWithAllLooseProperties(
DefinitionInfo mappingDefinition,
DefinitionInfo keyDefinition,
DefinitionInfo valueDefinition,
MappingToken mapping)
{
TemplateToken nextValue;
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
while (m_objectReader.AllowLiteral(out LiteralToken rawLiteral))
{
var nextKeyScalar = ParseScalar(rawLiteral, mappingDefinition.AllowedContext);
// Expression
if (nextKeyScalar is ExpressionToken)
{
// Legal
if (mappingDefinition.AllowedContext.Length > 0)
{
m_memory.AddBytes(nextKeyScalar);
nextValue = ReadValue(valueDefinition);
mapping.Add(nextKeyScalar, nextValue);
}
// Illegal
else
{
m_context.Error(nextKeyScalar, TemplateStrings.ExpressionNotAllowed());
SkipValue();
}
continue;
}
// Not a string, convert
if (!(nextKeyScalar is StringToken nextKey))
{
nextKey = new StringToken(nextKeyScalar.FileId, nextKeyScalar.Line, nextKeyScalar.Column, nextKeyScalar.ToString());
}
// Duplicate
if (!keys.Add(nextKey.Value))
{
m_context.Error(nextKey, TemplateStrings.ValueAlreadyDefined(nextKey.Value));
SkipValue();
continue;
}
// Validate
Validate(nextKey, keyDefinition);
m_memory.AddBytes(nextKey);
// Add the pair
nextValue = ReadValue(valueDefinition);
mapping.Add(nextKey, nextValue);
}
ExpectMappingEnd();
}
private void ExpectMappingEnd()
{
if (!m_objectReader.AllowMappingEnd())
{
throw new Exception("Expected mapping end"); // Should never happen
}
}
private void SkipValue(Boolean error = false)
{
m_memory.IncrementEvents();
// Scalar
if (m_objectReader.AllowLiteral(out LiteralToken literal))
{
if (error)
{
m_context.Error(literal, TemplateStrings.UnexpectedValue(literal));
}
return;
}
// Sequence
if (m_objectReader.AllowSequenceStart(out SequenceToken sequence))
{
m_memory.IncrementDepth();
if (error)
{
m_context.Error(sequence, TemplateStrings.UnexpectedSequenceStart());
}
while (!m_objectReader.AllowSequenceEnd())
{
SkipValue();
}
m_memory.DecrementDepth();
return;
}
// Mapping
if (m_objectReader.AllowMappingStart(out MappingToken mapping))
{
m_memory.IncrementDepth();
if (error)
{
m_context.Error(mapping, TemplateStrings.UnexpectedMappingStart());
}
while (!m_objectReader.AllowMappingEnd())
{
SkipValue();
SkipValue();
}
m_memory.DecrementDepth();
return;
}
// Unexpected
throw new InvalidOperationException(TemplateStrings.ExpectedScalarSequenceOrMapping());
}
private void Validate(
StringToken stringToken,
DefinitionInfo definition)
{
var scalar = stringToken as ScalarToken;
Validate(ref scalar, definition);
}
private void Validate(
ref ScalarToken scalar,
DefinitionInfo definition)
{
switch (scalar.Type)
{
case TokenType.Null:
case TokenType.Boolean:
case TokenType.Number:
case TokenType.String:
var literal = scalar as LiteralToken;
// Legal
if (definition.Get<ScalarDefinition>().Any(x => x.IsMatch(literal)))
{
return;
}
// Not a string, convert
if (literal.Type != TokenType.String)
{
literal = new StringToken(literal.FileId, literal.Line, literal.Column, literal.ToString());
// Legal
if (definition.Get<StringDefinition>().Any(x => x.IsMatch(literal)))
{
scalar = literal;
return;
}
}
// Illegal
m_context.Error(literal, TemplateStrings.UnexpectedValue(literal));
break;
case TokenType.BasicExpression:
// Illegal
if (definition.AllowedContext.Length == 0)
{
m_context.Error(scalar, TemplateStrings.ExpressionNotAllowed());
}
break;
default:
m_context.Error(scalar, TemplateStrings.UnexpectedValue(scalar));
break;
}
}
private ScalarToken ParseScalar(
LiteralToken token,
String[] allowedContext)
{
// Not a string
if (token.Type != TokenType.String)
{
return token;
}
// Check if the value is definitely a literal
var raw = token.ToString();
Int32 startExpression;
if (String.IsNullOrEmpty(raw) ||
(startExpression = raw.IndexOf(TemplateConstants.OpenExpression)) < 0) // Doesn't contain ${{
{
return token;
}
// Break the value into segments of LiteralToken and ExpressionToken
var segments = new List<ScalarToken>();
var i = 0;
while (i < raw.Length)
{
// An expression starts here:
if (i == startExpression)
{
// Find the end of the expression - i.e. }}
startExpression = i;
var endExpression = -1;
var inString = false;
for (i += TemplateConstants.OpenExpression.Length; i < raw.Length; i++)
{
if (raw[i] == '\'')
{
inString = !inString; // Note, this handles escaped single quotes gracefully. Ex. 'foo''bar'
}
else if (!inString && raw[i] == '}' && raw[i - 1] == '}')
{
endExpression = i;
i++;
break;
}
}
// Check if not closed
if (endExpression < startExpression)
{
m_context.Error(token, TemplateStrings.ExpressionNotClosed());
return token;
}
// Parse the expression
var rawExpression = raw.Substring(
startExpression + TemplateConstants.OpenExpression.Length,
endExpression - startExpression + 1 - TemplateConstants.OpenExpression.Length - TemplateConstants.CloseExpression.Length);
var expression = ParseExpression(token.Line, token.Column, rawExpression, allowedContext, out Exception ex);
// Check for error
if (ex != null)
{
m_context.Error(token, ex);
return token;
}
// Check if a directive was used when not allowed
if (!String.IsNullOrEmpty(expression.Directive) &&
((startExpression != 0) || (i < raw.Length)))
{
m_context.Error(token, TemplateStrings.DirectiveNotAllowedInline(expression.Directive));
return token;
}
// Add the segment
segments.Add(expression);
// Look for the next expression
startExpression = raw.IndexOf(TemplateConstants.OpenExpression, i);
}
// The next expression is further ahead:
else if (i < startExpression)
{
// Append the segment
AddString(segments, token.Line, token.Column, raw.Substring(i, startExpression - i));
// Adjust the position
i = startExpression;
}
// No remaining expressions:
else
{
AddString(segments, token.Line, token.Column, raw.Substring(i));
break;
}
}
// Check if can convert to a literal
// For example, the escaped expression: ${{ '{{ this is a literal }}' }}
if (segments.Count == 1 &&
segments[0] is BasicExpressionToken basicExpression &&
IsExpressionString(basicExpression.Expression, out String str))
{
return new StringToken(m_fileId, token.Line, token.Column, str);
}
// Check if only ony segment
if (segments.Count == 1)
{
return segments[0];
}
// Build the new expression, using the format function
var format = new StringBuilder();
var args = new StringBuilder();
var argIndex = 0;
foreach (var segment in segments)
{
if (segment is StringToken literal)
{
var text = ExpressionUtility.StringEscape(literal.Value) // Escape quotes
.Replace("{", "{{") // Escape braces
.Replace("}", "}}");
format.Append(text);
}
else
{
format.Append("{" + argIndex.ToString(CultureInfo.InvariantCulture) + "}"); // Append formatter
argIndex++;
var expression = segment as BasicExpressionToken;
args.Append(", ");
args.Append(expression.Expression);
}
}
return new BasicExpressionToken(m_fileId, token.Line, token.Column, $"format('{format}'{args})");
}
private ExpressionToken ParseExpression(
Int32? line,
Int32? column,
String value,
String[] allowedContext,
out Exception ex)
{
var trimmed = value.Trim();
// Check if the value is empty
if (String.IsNullOrEmpty(trimmed))
{
ex = new ArgumentException(TemplateStrings.ExpectedExpression());
return null;
}
// Try to find a matching directive
List<String> parameters;
if (MatchesDirective(trimmed, TemplateConstants.InsertDirective, 0, out parameters, out ex))
{
return new InsertExpressionToken(m_fileId, line, column);
}
else if (ex != null)
{
return null;
}
// Check if the value is an expression
if (!ExpressionToken.IsValidExpression(trimmed, allowedContext, out ex))
{
return null;
}
// Return the expression
return new BasicExpressionToken(m_fileId, line, column, trimmed);
}
private void AddString(
List<ScalarToken> segments,
Int32? line,
Int32? column,
String value)
{
// If the last segment was a LiteralToken, then append to the last segment
if (segments.Count > 0 && segments[segments.Count - 1] is StringToken lastSegment)
{
segments[segments.Count - 1] = new StringToken(m_fileId, line, column, lastSegment.Value + value);
}
// Otherwise add a new LiteralToken
else
{
segments.Add(new StringToken(m_fileId, line, column, value));
}
}
private static Boolean MatchesDirective(
String trimmed,
String directive,
Int32 expectedParameters,
out List<String> parameters,
out Exception ex)
{
if (trimmed.StartsWith(directive, StringComparison.Ordinal) &&
(trimmed.Length == directive.Length || Char.IsWhiteSpace(trimmed[directive.Length])))
{
parameters = new List<String>();
var startIndex = directive.Length;
var inString = false;
var parens = 0;
for (var i = startIndex; i < trimmed.Length; i++)
{
var c = trimmed[i];
if (Char.IsWhiteSpace(c) && !inString && parens == 0)
{
if (startIndex < i)
{
parameters.Add(trimmed.Substring(startIndex, i - startIndex));
}
startIndex = i + 1;
}
else if (c == '\'')
{
inString = !inString;
}
else if (c == '(' && !inString)
{
parens++;
}
else if (c == ')' && !inString)
{
parens--;
}
}
if (startIndex < trimmed.Length)
{
parameters.Add(trimmed.Substring(startIndex));
}
if (expectedParameters != parameters.Count)
{
ex = new ArgumentException(TemplateStrings.ExpectedNParametersFollowingDirective(expectedParameters, directive, parameters.Count));
parameters = null;
return false;
}
ex = null;
return true;
}
ex = null;
parameters = null;
return false;
}
private static Boolean IsExpressionString(
String trimmed,
out String str)
{
var builder = new StringBuilder();
var inString = false;
for (var i = 0; i < trimmed.Length; i++)
{
var c = trimmed[i];
if (c == '\'')
{
inString = !inString;
if (inString && i != 0)
{
builder.Append(c);
}
}
else if (!inString)
{
str = default;
return false;
}
else
{
builder.Append(c);
}
}
str = builder.ToString();
return true;
}
private struct DefinitionInfo
{
public DefinitionInfo(
TemplateSchema schema,
String name)
{
m_schema = schema;
// Lookup the definition
Definition = m_schema.GetDefinition(name);
// Record allowed context
AllowedContext = Definition.ReaderContext;
}
public DefinitionInfo(
DefinitionInfo parent,
String name)
{
m_schema = parent.m_schema;
// Lookup the definition
Definition = m_schema.GetDefinition(name);
// Record allowed context
if (Definition.ReaderContext.Length > 0)
{
AllowedContext = new HashSet<String>(parent.AllowedContext.Concat(Definition.ReaderContext), StringComparer.OrdinalIgnoreCase).ToArray();
}
else
{
AllowedContext = parent.AllowedContext;
}
}
public IEnumerable<T> Get<T>()
where T : Definition
{
return m_schema.Get<T>(Definition);
}
private TemplateSchema m_schema;
public Definition Definition;
public String[] AllowedContext;
}
private readonly TemplateContext m_context;
private readonly Int32? m_fileId;
private readonly TemplateMemory m_memory;
private readonly IObjectReader m_objectReader;
private readonly TemplateSchema m_schema;
}
}