Skip to content

Commit 8f12f08

Browse files
MattEdwardsWaggleBeevarndellwagglebeegithub-actions[bot]github-actions
authored
Release v1.3.2 (#57)
* [FEATURE]: Add Support for Raw Strings (#51) * Adds raw string literal parsing Implements a parser for raw string literals, allowing strings to span multiple lines and contain quotes without escaping. This simplifies string handling and improves code readability. --------- Co-authored-by: Matt Edwards <matthew.edwards@wagglebee.net> * [FEATURE]: Enhance TypeResolver for Member (#55) * Introduces ITypeResolver interface Refactors the TypeResolver class into an interface and an implementation. This allows for easier mocking and customization of type resolution logic. Also, this prepares the codebase for future extensions and testing. --------- Co-authored-by: Matt Edwards <matthew.edwards@wagglebee.net> * Refactors member access and indexer resolution Moves member access and indexer resolution logic from the parser to the type resolver. This change centralizes these operations within the type resolver, promoting code reuse and improving maintainability. It also removes duplicate code from the parser. * Updated code formatting to match rules in .editorconfig * Refactors type resolution for extensibility Separates type resolution and expression rewriting into distinct interfaces. This change improves the extensibility of the type resolution process by introducing the `ITypeRewriter` interface. The original `ITypeResolver` is now focused solely on resolving types and members, while the new interface handles the rewriting of expressions, such as indexers and member accesses. This allows for more flexible customization of how expressions are transformed during the parsing process. * Improves error handling and termination parsing Adds specific error messages for invalid termination, enhancing debugging. Prevents type parsing from misinterpreting labels as part of the type definition. Includes base message in SyntaxException to give full exception context. --------- Co-authored-by: annette.findley <annette.varndell@wagglebee.net> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com>
1 parent f6be213 commit 8f12f08

File tree

11 files changed

+312
-116
lines changed

11 files changed

+312
-116
lines changed

samples/extensions.dib

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!meta
22

3-
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"},{"aliases":[],"name":"razor"},{"aliases":[],"name":"xs"},{"aliases":[],"name":"xs-show"}]}}
3+
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"name":"csharp","languageName":"csharp"},{"name":"razor"},{"name":"xs"},{"name":"xs-show"},{"name":"fsharp","languageName":"F#","aliases":["f#","fs"]},{"name":"html","languageName":"HTML"},{"name":"http","languageName":"HTTP"},{"name":"javascript","languageName":"JavaScript","aliases":["js"]},{"name":"mermaid","languageName":"Mermaid"},{"name":"pwsh","languageName":"PowerShell","aliases":["powershell"]},{"name":"value"}]}}
44

55
#!pwsh
66

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Parlot;
2+
using Parlot.Fluent;
3+
4+
namespace Hyperbee.XS.Core.Parsers;
5+
6+
public class RawStringParser : Parser<TextSpan>
7+
{
8+
private enum ParserState
9+
{
10+
Initial,
11+
BeginContent,
12+
Content,
13+
EndContent
14+
}
15+
16+
public override bool Parse( ParseContext context, ref ParseResult<TextSpan> result )
17+
{
18+
context.EnterParser( this );
19+
20+
var scanner = context.Scanner;
21+
var cursor = scanner.Cursor;
22+
var start = cursor.Position;
23+
TextPosition begin = default;
24+
25+
scanner.SkipWhiteSpaceOrNewLine();
26+
27+
var state = ParserState.Initial;
28+
var quoteCount = 0;
29+
var requiredQuoteCount = 0;
30+
31+
while ( true )
32+
{
33+
var current = cursor.Current;
34+
35+
switch ( state )
36+
{
37+
case ParserState.Initial:
38+
if ( current == '"' )
39+
{
40+
quoteCount++;
41+
}
42+
else if ( quoteCount >= 3 )
43+
{
44+
state = ParserState.BeginContent;
45+
requiredQuoteCount = quoteCount;
46+
begin = scanner.Cursor.Position;
47+
}
48+
else
49+
{
50+
scanner.Cursor.ResetPosition( start );
51+
context.ExitParser( this );
52+
return false;
53+
}
54+
break;
55+
56+
case ParserState.BeginContent:
57+
state = ParserState.Content;
58+
quoteCount = 0;
59+
break;
60+
61+
case ParserState.Content:
62+
if ( current == '"' )
63+
{
64+
quoteCount++;
65+
if ( quoteCount == requiredQuoteCount )
66+
{
67+
state = ParserState.EndContent;
68+
}
69+
}
70+
else
71+
{
72+
quoteCount = 0;
73+
}
74+
break;
75+
76+
case ParserState.EndContent:
77+
var end = scanner.Cursor.Position.Offset;
78+
79+
var decoded = Character.DecodeString(
80+
new TextSpan( scanner.Buffer, begin.Offset, end - begin.Offset - requiredQuoteCount )
81+
);
82+
83+
result.Set( start.Offset, end, decoded );
84+
context.ExitParser( this );
85+
cursor.Advance();
86+
87+
return true;
88+
89+
default:
90+
throw new ParseException( $"Invalid state for {nameof( RawStringParser )} found.", start );
91+
}
92+
93+
cursor.Advance();
94+
95+
if ( !cursor.Eof )
96+
continue;
97+
98+
scanner.Cursor.ResetPosition( start );
99+
context.ExitParser( this );
100+
return false;
101+
}
102+
}
103+
}

src/Hyperbee.XS/Core/Parsers/TerminationParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ public static Parser<T> WithTermination<T>( this Parser<T> parser )
1919
{
2020
return parser.AndSkipIf(
2121
( ctx, _ ) => ((XsContext) ctx).RequireTermination,
22-
OneOrMany( Terms.Char( ';' ) ),
23-
ZeroOrMany( Terms.Char( ';' ) ).RequireTermination( true )
22+
OneOrMany( Terms.Char( ';' ) ).ElseError( XsParser.InvalidTerminationMessage ),
23+
ZeroOrMany( Terms.Char( ';' ) ).RequireTermination( true ).ElseError( XsParser.InvalidTerminationMessage )
2424
).Named( "Termination" );
2525
}
2626
}

src/Hyperbee.XS/Core/Parsers/TypeRuntimeParser.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ public override bool Parse( ParseContext context, ref ParseResult<Type> result )
5151
scanner.SkipWhiteSpaceOrNewLine();
5252
}
5353

54+
if ( scanner.Cursor.Current == ':' )
55+
{
56+
// Invalid for types to end with a colon (Identifier is being used as a label)
57+
cursor.ResetPosition( start );
58+
context.ExitParser( this );
59+
return false;
60+
}
61+
5462
// get any generic argument types
5563

5664
var genericArgs = new List<Type>();

src/Hyperbee.XS/Core/TypeResolver.cs

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@
66

77
namespace Hyperbee.XS.Core;
88

9-
public sealed class TypeResolver
9+
public interface ITypeResolver
10+
{
11+
Type ResolveType( string typeName );
12+
MethodInfo ResolveMethod( Type type, string methodName, IReadOnlyList<Type> typeArgs, IReadOnlyList<Expression> args );
13+
MemberInfo ResolveMember( Type type, string memberName );
14+
}
15+
public interface ITypeRewriter
16+
{
17+
Expression RewriteIndexerExpression( Expression targetExpression, IReadOnlyList<Expression> indexes );
18+
Expression RewriteMemberExpression( Expression targetExpression, string name, IReadOnlyList<Type> typeArgs, IReadOnlyList<Expression> args );
19+
}
20+
21+
public class TypeResolver : ITypeResolver, ITypeRewriter
1022
{
1123
public ReferenceManager ReferenceManager { get; }
1224

@@ -53,7 +65,7 @@ public TypeResolver( ReferenceManager referenceManager )
5365
ReferenceManager = referenceManager;
5466
}
5567

56-
public Type ResolveType( string typeName )
68+
public virtual Type ResolveType( string typeName )
5769
{
5870
return _typeCache.GetOrAdd( typeName, _ =>
5971
{
@@ -93,7 +105,7 @@ static Type GetTypeFromKeyword( string typeName )
93105
}
94106
}
95107

96-
public MethodInfo ResolveMethod( Type type, string methodName, IReadOnlyList<Type> typeArgs, IReadOnlyList<Expression> args )
108+
public virtual MethodInfo ResolveMethod( Type type, string methodName, IReadOnlyList<Type> typeArgs, IReadOnlyList<Expression> args )
97109
{
98110
var candidateMethods = GetCandidateMethods( methodName, type );
99111
var callerTypes = GetCallerTypes( type, args );
@@ -153,6 +165,106 @@ public MethodInfo ResolveMethod( Type type, string methodName, IReadOnlyList<Typ
153165
return bestMatch;
154166
}
155167

168+
public virtual MemberInfo ResolveMember( Type type, string memberName )
169+
{
170+
const BindingFlags BindingAttr = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
171+
return type.GetMember( memberName, BindingAttr ).FirstOrDefault();
172+
}
173+
174+
public virtual Expression RewriteIndexerExpression( Expression targetExpression, IReadOnlyList<Expression> indexes )
175+
{
176+
var indexers = targetExpression.Type.GetProperties()
177+
.Where( p => p.GetIndexParameters().Length == indexes.Count )
178+
.ToArray();
179+
180+
if ( indexers.Length == 0 )
181+
return Expression.ArrayAccess( targetExpression, indexes );
182+
183+
// Find the best match based on parameter types
184+
var indexer = indexers.FirstOrDefault( p =>
185+
p.GetIndexParameters()
186+
.Select( param => param.ParameterType )
187+
.SequenceEqual( indexes.Select( i => i.Type ) ) );
188+
189+
if ( indexer == null )
190+
{
191+
throw new InvalidOperationException(
192+
$"No matching indexer found on type '{targetExpression.Type}' with parameter types: " +
193+
$"{string.Join( ", ", indexes.Select( i => i.Type.Name ) )}." );
194+
}
195+
196+
return Expression.Property( targetExpression, indexer, indexes.ToArray() );
197+
}
198+
199+
public virtual Expression RewriteMemberExpression( Expression targetExpression, string name, IReadOnlyList<Type> typeArgs, IReadOnlyList<Expression> args )
200+
{
201+
var type = TypeOf( targetExpression );
202+
203+
// method
204+
205+
if ( args != null )
206+
{
207+
var method = ResolveMethod( type, name, typeArgs, args );
208+
209+
if ( method == null )
210+
throw new InvalidOperationException( $"Method '{name}' not found on type '{type}'." );
211+
212+
var arguments = GetArgumentsWithDefaults( method, targetExpression, args );
213+
214+
return method.IsStatic
215+
? Expression.Call( method, arguments )
216+
: Expression.Call( targetExpression, method, arguments );
217+
}
218+
219+
// property or field
220+
221+
var member = ResolveMember( type, name );
222+
223+
if ( member == null )
224+
throw new InvalidOperationException( $"Member '{name}' not found on type '{type}'." );
225+
226+
return member switch
227+
{
228+
PropertyInfo property => Expression.Property( targetExpression, property ),
229+
FieldInfo field => Expression.Field( targetExpression, field ),
230+
_ => throw new InvalidOperationException( $"Member '{name}' is not a property or field." )
231+
};
232+
233+
static IReadOnlyList<Expression> GetArgumentsWithDefaults( MethodInfo method, Expression targetExpression, IReadOnlyList<Expression> providedArgs )
234+
{
235+
var parameters = method.GetParameters();
236+
var isExtension = method.IsDefined( typeof( ExtensionAttribute ), false );
237+
238+
var providedOffset = isExtension ? 1 : 0;
239+
var providedCount = providedArgs.Count;
240+
var totalParameters = parameters.Length;
241+
242+
if ( providedCount == totalParameters )
243+
return providedArgs;
244+
245+
var methodArgs = new Expression[totalParameters];
246+
247+
// add provided arguments
248+
if ( isExtension )
249+
methodArgs[0] = targetExpression;
250+
251+
for ( var i = 0; i < providedCount; i++ )
252+
{
253+
methodArgs[i + providedOffset] = providedArgs[i];
254+
}
255+
256+
// add missing optional parameters
257+
for ( var i = providedCount + providedOffset; i < totalParameters; i++ )
258+
{
259+
methodArgs[i] = parameters[i].HasDefaultValue
260+
? Expression.Constant( parameters[i].DefaultValue, parameters[i].ParameterType )
261+
: throw new ArgumentException( $"Missing required parameter: {parameters[i].Name}" );
262+
}
263+
264+
return methodArgs;
265+
}
266+
}
267+
156268
public void RegisterExtensionMethods( IEnumerable<Assembly> assemblies )
157269
{
158270
Parallel.ForEach( assemblies, assembly =>
@@ -386,5 +498,18 @@ private static int CompareMethods( MethodInfo m1, MethodInfo m2 )
386498

387499
return p1.Length.CompareTo( p2.Length );
388500
}
501+
502+
[MethodImpl( MethodImplOptions.AggressiveInlining )]
503+
private static Type TypeOf( Expression expression )
504+
{
505+
ArgumentNullException.ThrowIfNull( expression, nameof( expression ) );
506+
507+
return expression switch
508+
{
509+
ConstantExpression ce => ce.Value as Type ?? ce.Type,
510+
_ => expression.Type
511+
};
512+
}
513+
389514
}
390515

src/Hyperbee.XS/SyntaxException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public override string Message
3030
var sourceLine = BufferHelper.GetLine( Buffer, Offset, out var caret );
3131
var caretLine = new string( ' ', caret ) + "^";
3232

33-
return $"({Line} {Column})\n{sourceLine}\n{caretLine}";
33+
return $"{base.Message}\n({Line} {Column})\n{sourceLine}\n{caretLine}";
3434
}
3535
}
3636
}

src/Hyperbee.XS/XsParser.Identifiers.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Hyperbee.XS;
88
public partial class XsParser
99
{
1010
public const string InvalidSyntaxMessage = "Invalid syntax.";
11+
public const string InvalidTerminationMessage = "Invalid termination, missing ';'.";
1112
public const string InvalidTypeMessage = "Invalid type.";
1213
public const string InvalidExpressionMessage = "Invalid expression.";
1314
public const string InvalidStatementMessage = "Invalid statement.";
@@ -21,7 +22,7 @@ public partial class XsParser
2122
internal static Parser<char> CloseBracket = Terms.Char( ']' ).ElseError( InvalidSyntaxMessage );
2223
internal static Parser<char> Assignment = Terms.Char( '=' ).ElseError( InvalidSyntaxMessage );
2324
internal static Parser<char> Delimiter = Terms.Char( ',' ).ElseError( InvalidSyntaxMessage );
24-
internal static Parser<char> Terminator = Terms.Char( ';' ).ElseError( InvalidSyntaxMessage );
25+
internal static Parser<char> Terminator = Terms.Char( ';' ).ElseError( InvalidTerminationMessage );
2526
internal static Parser<char> Colon = Terms.Char( ':' ).ElseError( InvalidSyntaxMessage );
2627
}
2728

src/Hyperbee.XS/XsParser.Literals.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ private static Parser<Expression> LiteralParser( XsConfig config, Deferred<Expre
3535
var stringLiteral = Terms.String( StringLiteralQuotes.Double )
3636
.Then<Expression>( static value => Constant( value.ToString() ) );
3737

38+
var rawStringLiteral = new RawStringParser().
39+
Then<Expression>( static value => Constant( value.ToString() ) );
40+
3841
var nullLiteral = Terms.Text( "null" )
3942
.Then<Expression>( static _ => Constant( null ) );
4043

@@ -43,6 +46,7 @@ private static Parser<Expression> LiteralParser( XsConfig config, Deferred<Expre
4346
doubleLiteral,
4447
floatLiteral,
4548
integerLiteral,
49+
rawStringLiteral,
4650
characterLiteral,
4751
stringLiteral,
4852
booleanLiteral,

0 commit comments

Comments
 (0)