Skip to content

Commit

Permalink
More updates... Getting closer.
Browse files Browse the repository at this point in the history
  • Loading branch information
IEvangelist committed Sep 12, 2023
1 parent 6de57e8 commit 471581c
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 37 deletions.
20 changes: 20 additions & 0 deletions src/Blazor.SourceGenerators/Extensions/KeyValuePairExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace Blazor.SourceGenerators.Extensions;

internal static class KeyValuePairExtensions
{
/// <summary>
/// Extends the <see cref="KeyValuePair{TKey, TValue}"/> type,
/// by exposing the <c>Deconstruct</c> functionality. Meaning that
/// we can now do the following:
/// <c>foreach (var (key, value) in Dictionary) { ... }</c>
/// </summary>
/// <typeparam name="TKey">The key <c>TKey</c> type.</typeparam>
/// <typeparam name="TValue">The value <c>TValue</c> type.</typeparam>
internal static void Deconstruct<TKey, TValue>(
this KeyValuePair<TKey, TValue> kvp,
out TKey key,
out TValue value) => (key, value) = (kvp.Key, kvp.Value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ namespace Blazor.SourceGenerators.TypeScript;

public interface ITypeScriptAbstractSyntaxTree
{
string RawSourceText { get; set; }
/// <summary>
/// Gets the raw source text used to parse the abstract syntax tree.
/// </summary>
string RawSourceText { get; }

RootNodeSourceFile RootNode { get; set; }
/// <summary>
/// Gets the root node (<see cref="RootNodeSourceFile"/>) of the abstract syntax tree.
/// </summary>
RootNodeSourceFile RootNode { get; }

void ParseAsAst(string source, string fileName = "app.ts");
/// <summary>
/// Gets the script target (<see cref="ScriptTarget"/>) of the abstract syntax tree."/>
/// </summary>
ScriptTarget Target { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,53 @@ namespace Blazor.SourceGenerators.TypeScript;

internal sealed class TypeScriptAbstractSyntaxTree : ITypeScriptAbstractSyntaxTree
{
private readonly ScriptTarget _languageVersion;
public ScriptTarget Target { get; }

public string RawSourceText { get; set; }
public RootNodeSourceFile RootNode { get; set; }
public string RawSourceText { get; }

public TypeScriptAbstractSyntaxTree(
string source = null,
public RootNodeSourceFile RootNode { get; }

private TypeScriptAbstractSyntaxTree(
string sourceText = null,
string fileName = "app.ts",
ScriptTarget languageVersion = ScriptTarget.Latest)
ScriptTarget target = ScriptTarget.Latest)
{
_languageVersion = languageVersion;
if (source is not null)
Target = target;

if (string.IsNullOrWhiteSpace(sourceText))
{
ParseAsAst(source, fileName);
throw new ArgumentNullException(nameof(sourceText));
}
}

public void ParseAsAst(string source, string fileName = "app.ts")
{
RawSourceText = source;
RawSourceText = sourceText;
var parser = new Parser();
RootNode = parser.ParseSourceFile(
fileName,
source,
_languageVersion,
sourceText,
Target,
true,
ScriptKind.Ts);
RootNode.AbstractSyntaxTree = this;
RootNode.ParseChildren(this);
}

/// <summary>
/// Gets a representation of the TypeScript abstract syntax tree from the given
/// <paramref name="sourceText"/> and <paramref name="fileName"/>.
/// </summary>
/// <param name="sourceText">
/// The source text to parse the abstract syntax tree from.
/// </param>
/// <param name="fileName">
/// The name of the file to parse the abstract syntax tree from.
/// </param>
/// <param name="target">
/// The target script version to parse the abstract syntax tree from.
/// </param>
/// <returns>An instance of <see cref="ITypeScriptAbstractSyntaxTree"/> instance.</returns>
public static ITypeScriptAbstractSyntaxTree FromSourceText(
string sourceText,
string fileName = "lib.dom.d.ts",
ScriptTarget target = ScriptTarget.Latest) =>
new TypeScriptAbstractSyntaxTree(sourceText, fileName, target);
}
4 changes: 2 additions & 2 deletions src/Blazor.SourceGenerators/TypeScript/Types/INode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ public interface INode : ITextRange
TypeScriptType ContextualType { get; set; }
TypeMapper ContextualMapper { get; set; }
int TagInt { get; set; }
string GetText(string source = null);
string GetTextWithComments(string source = null);
ReadOnlySpan<char> GetText(string source = null);
ReadOnlySpan<char> GetTextWithComments(string source = null);
string GetTreeString(bool withPos = true);
string ToString(bool withPos);
Node First { get; }
Expand Down
39 changes: 23 additions & 16 deletions src/Blazor.SourceGenerators/TypeScript/Types/Node.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@
// Licensed under the MIT License.

#nullable disable
using System.Diagnostics;
using Blazor.SourceGenerators.TypeScript.Compiler;

// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

#nullable disable
namespace Blazor.SourceGenerators.TypeScript.Types;

[DebuggerDisplay("{SourceText}")]
public class Node : TextRange, INode
{
public List<Node> Children { get; set; } = new();
public ITypeScriptAbstractSyntaxTree AbstractSyntaxTree { get; set; }

public string RawSourceText => AbstractSyntaxTree.RawSourceText;

public string SourceText => GetText().ToString();

public string Identifier => Kind is TypeScriptSyntaxKind.Identifier
? GetText()
: Children.FirstOrDefault(v => v.Kind is TypeScriptSyntaxKind.Identifier)?.GetText().Trim();
? GetText().ToString()
: Children.FirstOrDefault(v => v.Kind is TypeScriptSyntaxKind.Identifier)
?.GetText()
.Trim()
.ToString();

public int ParentId { get; set; }
public int Depth { get; set; }
Expand Down Expand Up @@ -63,23 +66,27 @@ public void ParseChildren(ITypeScriptAbstractSyntaxTree abstractSyntaxTree) =>
return null;
});

public string GetText(string source = null)
public ReadOnlySpan<char> GetText(string source = null)
{
source ??= RawSourceText;

return NodeStart is -1
? Pos.HasValue && End.HasValue
? source.SubString(Pos.Value, End.Value)
: null
: End.HasValue ? source.SubString(NodeStart, End.Value) : null;
var (start, end) = (Pos ?? NodeStart, End.Value);

return start is 0 || end is 0
? null
: source.AsSpan(start, end - start);
}

public string GetTextWithComments(string source = null)
public ReadOnlySpan<char> GetTextWithComments(string source = null)
{
source ??= RawSourceText;
return Pos != null && End != null
? source.Substring((int)Pos, (int)End - (int)Pos)
: null;

var (start, end) =
(Pos.GetValueOrDefault(-1), End.GetValueOrDefault(-1));

return start is -1 || end is -1
? null
: source.AsSpan(start, end - start);
}

public override string ToString() => ToString(true);
Expand Down
124 changes: 124 additions & 0 deletions src/Blazor.SourceGenerators/Types/DependentTypeMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Reflection.Metadata;
using Blazor.SourceGenerators.TypeScript;
using Blazor.SourceGenerators.TypeScript.Types;

namespace Blazor.SourceGenerators.Types;

internal static class DependentTypeMapper
{
private static readonly Lazy<ITypeScriptAbstractSyntaxTree> s_lazyDefaultAst = new(() =>
{
var reader = TypeDeclarationReader.Default;
return TypeScriptAbstractSyntaxTree.FromSourceText(reader.RawSourceText);
});

/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> where the <c>TKey</c> is a
/// <see cref="string"/> and the <c>TValue</c> is a <see cref="Node"/>.
/// </summary>
/// <param name="interfaceName">The name of the <c>Name</c> in the TypeScript
/// _.d.ts_ file's <c>interface name { ... }</c> definition.</param>
/// <returns>A representation of all the dependent types as a
/// <see cref="Dictionary{TKey, TValue}"/> where the <c>TKey</c> is a
/// <see cref="string"/> and the <c>TValue</c> is a <see cref="Node"/>.
/// Returns empty when the underlying AST parser is null or has no
/// root node, or if there isn't an <c>interface</c> found.
/// </returns>
public static Dictionary<string, Node> GetDependentTypeMap(string interfaceName)
{
var ast = s_lazyDefaultAst.Value;
if (ast is null or { RootNode: null })
{
return new();
}

var rootNode = ast.RootNode;

var interfaceNode = rootNode.Children
.OfKind(TypeScriptSyntaxKind.InterfaceDeclaration)
.FirstOrDefault(type => type.Identifier == interfaceName);

if (interfaceNode is null)
{
return new Dictionary<string, Node>();
}

var dependentTypes = new Dictionary<string, Node>
{
[interfaceNode.Identifier] = interfaceNode
};

var queue = new Queue<Node>();
queue.Enqueue(interfaceNode);

while (queue.Count > 0)
{
var node = queue.Dequeue();

// Get methods
var methods =
node.OfKind(TypeScriptSyntaxKind.MethodSignature).Cast<MethodSignature>();

foreach (var method in methods)
{
// Get method parameters
var parameters =
method.OfKind(TypeScriptSyntaxKind.Parameter).Cast<ParameterDeclaration>();

foreach (var parameter in parameters)
{
// Get parameter type
var typeReferences =
parameter.OfKind(TypeScriptSyntaxKind.TypeReference)
.Cast<TypeReferenceNode>();

foreach (var typeReference in typeReferences)
{
dependentTypes[typeReference.Identifier] = typeReference;
queue.Enqueue(typeReference);

foreach ((var key, var @interface) in
GetDependentTypeMap(interfaceName: typeReference.Identifier))
{
dependentTypes[@interface.Identifier] = @interface;
queue.Enqueue(@interface);
}
}
}

// Get method return type
var returnTypeReferences =
method.OfKind(TypeScriptSyntaxKind.TypeReference)
.Cast<TypeReferenceNode>();

foreach (var typeReference in returnTypeReferences)
{
dependentTypes[typeReference.Identifier] = typeReference;
queue.Enqueue(typeReference);
}
}

// Get properties
var properties =
node.OfKind(TypeScriptSyntaxKind.PropertySignature)
.Cast<PropertySignature>();

foreach (var property in properties)
{
dependentTypes[property.Identifier] = property;
queue.Enqueue(property);
}

foreach (var childNode in node.Children)
{
queue.Enqueue(childNode);
}
}

return dependentTypes;
}
}
3 changes: 2 additions & 1 deletion tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class LibDomParserTests
public LibDomParserTests()
{
var reader = TypeDeclarationReader.Default;
_sut = new TypeScriptAbstractSyntaxTree(reader.RawSourceText);
_sut = TypeScriptAbstractSyntaxTree.FromSourceText(reader.RawSourceText);
}

[Fact]
Expand All @@ -37,6 +37,7 @@ public void CanReplaceBruteForceParser()
var cacheStorage =
_sut.RootNode.OfKind(TypeScriptSyntaxKind.InterfaceDeclaration)
.Single(c => c is { Identifier: "CacheStorage" });

Assert.NotNull(cacheStorage);

var methods = cacheStorage.OfKind(TypeScriptSyntaxKind.MethodSignature).Cast<MethodSignature>();
Expand Down
51 changes: 51 additions & 0 deletions tests/Blazor.SourceGenerators.Tests/TypeMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics;
using Blazor.SourceGenerators.Types;
using Blazor.SourceGenerators.TypeScript.Types;
using Xunit;

namespace Blazor.SourceGenerators.Tests;

public class TypeMapperTests
{
[Fact]
public void TypeMapperCorrectlyMapsKnownTypeMap()
{
static Dictionary<string, Node> GetTypeMapWithPotential(bool timePenalty)
{
var startingTimestamp = Stopwatch.GetTimestamp();
var sut = DependentTypeMapper.GetDependentTypeMap;

// The implementation: window.navigator.geolocation
var typeMap = sut("Geolocation");

Assert.NotEmpty(typeMap);
var elapsedTimestamp = Stopwatch.GetElapsedTime(startingTimestamp);

// This needs to take less than a second.
// But only fail the test if there is a time penalty.
Assert.True(
condition: timePenalty is false ||
elapsedTimestamp.TotalMilliseconds < 1_000, $"""
condition: timePenalty is {timePenalty is false}
or took longer than 1,000ms {elapsedTimestamp.TotalMilliseconds < 1_000}.
""");

return typeMap;
}

var typeMap = GetTypeMapWithPotential(timePenalty: false);

Assert.NotNull(typeMap["Geolocation"]);
Assert.NotNull(typeMap["PositionCallback"]);
Assert.NotNull(typeMap["PositionErrorCallback"]);
Assert.NotNull(typeMap["PositionOptions"]);

// TODO: these types should be present, but they're not...
// Assert.NotNull(typeMap["GeolocationPosition"]);
// Assert.NotNull(typeMap["GeolocationPositionError"]);
// Assert.NotNull(typeMap["GeolocationCoordinates"]);
}
}

0 comments on commit 471581c

Please sign in to comment.