using System; using SharpGen.Logging; using SharpGen.Model; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace SharpGen.Transform { internal static class RelationParser { private const string StructSizeLegacy = "struct-size"; private const string StructSizeReplacement = nameof(StructSizeRelation); private const string ConstLegacy = "const"; private const string ConstReplacement = nameof(ConstantValueRelation); private const string WrapPrefix = "new[] { "; private const string WrapSuffix = " }"; private static CSharpParseOptions _sharpParseOptions; internal static CSharpParseOptions SharpParseOptions => _sharpParseOptions ??= new CSharpParseOptions(LanguageVersion.Preview); public static IReadOnlyList ParseRelation(string relation, Logger logger) { if (string.IsNullOrWhiteSpace(relation)) return null; var wrappedRelations = WrapRelation( relation, out var structSizeReplacementCount, out var constReplacementCount ); try { var tree = SyntaxFactory.ParseExpression(wrappedRelations, options: SharpParseOptions); if (tree is not ImplicitArrayCreationExpressionSyntax arrayCreationTree) { logger.Error(LoggingCodes.InvalidRelation, "Relation [{0}] parse failed: internal expression wrapping error. Ignoring.", relation); return null; } var relationsList = arrayCreationTree.Initializer.Expressions; var result = relationsList.Select(ParseSingleRelation).ToArray(); if (result.All(x => x.Relation != null)) { // Success, do diagnostics and return result. if (structSizeReplacementCount != result.Count(x => x.Relation is StructSizeRelation && x.Identifier == StructSizeReplacement)) logger.Warning( LoggingCodes.InvalidRelation, "Relation [{0}] replaced extra \"{1}\" substrings.", relation, StructSizeLegacy ); if (constReplacementCount != result.Count(x => x.Relation is ConstantValueRelation && x.Identifier == ConstReplacement)) logger.Warning( LoggingCodes.InvalidRelation, "Relation [{0}] replaced extra \"{1}\" substrings.", relation, ConstLegacy ); return result.Select(x => x.Relation).ToArray(); } // At least one relation failed to parse. foreach (var (failedResult, i) in result.Select((x, i) => (Result: x, i)).Where(x => x.Result.Relation == null)) { // If source of the failure is too short, it wouldn't look good or helpful. // Use full span of that failed relation instead. var sourceSpan = failedResult.IssueSource.Length > 3 ? failedResult.IssueSource : relationsList[i].Span; logger.Error( LoggingCodes.InvalidRelation, "Sub-relation [{0}] parse failed: {1}.", wrappedRelations.Substring(sourceSpan.Start, sourceSpan.Length), failedResult.IssueDescription ); } logger.Error(LoggingCodes.InvalidRelation, "Relation [{0}] parse failed due to the previous errors. Ignoring.", relation); return null; } catch (Exception exception) { logger.Error( LoggingCodes.InvalidRelation, "Relation [{0}] parse failed: {1} — {2}. Ignoring.", exception, relation, exception.GetType().Name, exception.Message ); return null; } } private static string WrapRelation(string relation, out uint structSizeReplacementCount, out uint constReplacementCount) { var wrappedRelationBuilder = new StringBuilder(WrapPrefix, WrapPrefix.Length + relation.Length + WrapSuffix.Length); structSizeReplacementCount = 0; constReplacementCount = 0; var offset = 0; while (true) { var structSizeIndex = relation.IndexOf(StructSizeLegacy, offset, StringComparison.Ordinal); var constIndex = relation.IndexOf(ConstLegacy, offset, StringComparison.Ordinal); var indexes = new[] {structSizeIndex, constIndex}.Where(x => x != -1).ToArray(); if (indexes.Length == 0) { wrappedRelationBuilder.Append(relation, offset, relation.Length - offset); break; } var index = indexes.Min(); wrappedRelationBuilder.Append(relation, offset, index - offset); if (index == structSizeIndex) { ++structSizeReplacementCount; wrappedRelationBuilder.Append(StructSizeReplacement); offset = index + StructSizeLegacy.Length; } else if (index == constIndex) { ++constReplacementCount; wrappedRelationBuilder.Append(ConstReplacement); offset = index + ConstLegacy.Length; } else { throw new Exception($"Internal {nameof(RelationParser)} indexes error"); } } wrappedRelationBuilder.Append(WrapSuffix); return wrappedRelationBuilder.ToString(); } private static ParseResult ParseSingleRelation(ExpressionSyntax item) { if (item is not InvocationExpressionSyntax invocationExpression) return new ParseResult(ParseResult.ParseIssue.RelationInvocationExpected, item.Span); var functionNameExpression = invocationExpression.Expression; if (functionNameExpression is not IdentifierNameSyntax functionIdentifier) return new ParseResult( ParseResult.ParseIssue.SimpleIdentifierAsFunctionNameExpected, functionNameExpression.Span ); var argumentList = invocationExpression.ArgumentList.Arguments; ParseResult ParseLengthRelation(string identifier) { return argumentList.Count switch { 1 => new ParseResult( new LengthRelation { Identifier = argumentList[0].Expression.ToString() }, identifier ), _ => new ParseResult(ParseResult.ParseIssue.ArgumentCountMismatch, argumentList.Span) }; } ParseResult ParseConstantValueRelation(string identifier) { return argumentList.Count switch { 1 => new ParseResult( new ConstantValueRelation { Value = argumentList[0].Expression }, identifier ), _ => new ParseResult(ParseResult.ParseIssue.ArgumentCountMismatch, argumentList.Span) }; } ParseResult ParseStructSizeRelation(string identifier) { return argumentList.Count switch { 0 => new ParseResult(new StructSizeRelation(), identifier), _ => new ParseResult(ParseResult.ParseIssue.ArgumentCountMismatch, argumentList.Span) }; } var identifierText = functionIdentifier.Identifier.ValueText; return identifierText switch { "length" => ParseLengthRelation(identifierText), "Length" => ParseLengthRelation(identifierText), ConstReplacement => ParseConstantValueRelation(identifierText), "Const" => ParseConstantValueRelation(identifierText), StructSizeReplacement => ParseStructSizeRelation(identifierText), "StructSize" => ParseStructSizeRelation(identifierText), _ => new ParseResult(ParseResult.ParseIssue.UnknownRelation, functionIdentifier.Span) }; } private sealed class ParseResult { public ParseResult(MarshallableRelation relation, string identifier) { Relation = relation ?? throw new ArgumentNullException(nameof(relation)); Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier)); } public ParseResult(ParseIssue issue, TextSpan issueSource) { Issue = issue; IssueSource = issueSource; } public MarshallableRelation Relation { get; } public string Identifier { get; } public ParseIssue Issue { get; } public TextSpan IssueSource { get; } public enum ParseIssue { ArgumentCountMismatch, UnknownRelation, RelationInvocationExpected, SimpleIdentifierAsFunctionNameExpected } public string IssueDescription => Issue switch { ParseIssue.ArgumentCountMismatch => "mismatched argument count", ParseIssue.UnknownRelation => "unknown relation name", ParseIssue.RelationInvocationExpected => "expected relation invocation", ParseIssue.SimpleIdentifierAsFunctionNameExpected => "expected simple identifier as relation name", _ => throw new Exception($"Unknown {nameof(ParseIssue)}") }; } } }