diff --git a/src/Blazor.SourceGenerators/Extensions/KeyValuePairExtensions.cs b/src/Blazor.SourceGenerators/Extensions/KeyValuePairExtensions.cs
new file mode 100644
index 0000000..1b49d7f
--- /dev/null
+++ b/src/Blazor.SourceGenerators/Extensions/KeyValuePairExtensions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Blazor.SourceGenerators.Extensions;
+
+internal static class KeyValuePairExtensions
+{
+ ///
+ /// Extends the type,
+ /// by exposing the Deconstruct functionality. Meaning that
+ /// we can now do the following:
+ /// foreach (var (key, value) in Dictionary) { ... }
+ ///
+ /// The key TKey type.
+ /// The value TValue type.
+ internal static void Deconstruct(
+ this KeyValuePair kvp,
+ out TKey key,
+ out TValue value) => (key, value) = (kvp.Key, kvp.Value);
+}
diff --git a/src/Blazor.SourceGenerators/TypeScript/ITypeScriptAbstractSyntaxTree.cs b/src/Blazor.SourceGenerators/TypeScript/ITypeScriptAbstractSyntaxTree.cs
index 8e68da9..f1b0730 100644
--- a/src/Blazor.SourceGenerators/TypeScript/ITypeScriptAbstractSyntaxTree.cs
+++ b/src/Blazor.SourceGenerators/TypeScript/ITypeScriptAbstractSyntaxTree.cs
@@ -7,9 +7,18 @@ namespace Blazor.SourceGenerators.TypeScript;
public interface ITypeScriptAbstractSyntaxTree
{
- string RawSourceText { get; set; }
+ ///
+ /// Gets the raw source text used to parse the abstract syntax tree.
+ ///
+ string RawSourceText { get; }
- RootNodeSourceFile RootNode { get; set; }
+ ///
+ /// Gets the root node () of the abstract syntax tree.
+ ///
+ RootNodeSourceFile RootNode { get; }
- void ParseAsAst(string source, string fileName = "app.ts");
+ ///
+ /// Gets the script target () of the abstract syntax tree."/>
+ ///
+ ScriptTarget Target { get; }
}
diff --git a/src/Blazor.SourceGenerators/TypeScript/TypeScriptAbstractSyntaxTree.cs b/src/Blazor.SourceGenerators/TypeScript/TypeScriptAbstractSyntaxTree.cs
index e926874..4699110 100644
--- a/src/Blazor.SourceGenerators/TypeScript/TypeScriptAbstractSyntaxTree.cs
+++ b/src/Blazor.SourceGenerators/TypeScript/TypeScriptAbstractSyntaxTree.cs
@@ -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);
}
+
+ ///
+ /// Gets a representation of the TypeScript abstract syntax tree from the given
+ /// and .
+ ///
+ ///
+ /// The source text to parse the abstract syntax tree from.
+ ///
+ ///
+ /// The name of the file to parse the abstract syntax tree from.
+ ///
+ ///
+ /// The target script version to parse the abstract syntax tree from.
+ ///
+ /// An instance of instance.
+ public static ITypeScriptAbstractSyntaxTree FromSourceText(
+ string sourceText,
+ string fileName = "lib.dom.d.ts",
+ ScriptTarget target = ScriptTarget.Latest) =>
+ new TypeScriptAbstractSyntaxTree(sourceText, fileName, target);
}
diff --git a/src/Blazor.SourceGenerators/TypeScript/Types/INode.cs b/src/Blazor.SourceGenerators/TypeScript/Types/INode.cs
index 4c410d2..3431126 100644
--- a/src/Blazor.SourceGenerators/TypeScript/Types/INode.cs
+++ b/src/Blazor.SourceGenerators/TypeScript/Types/INode.cs
@@ -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 GetText(string source = null);
+ ReadOnlySpan GetTextWithComments(string source = null);
string GetTreeString(bool withPos = true);
string ToString(bool withPos);
Node First { get; }
diff --git a/src/Blazor.SourceGenerators/TypeScript/Types/Node.cs b/src/Blazor.SourceGenerators/TypeScript/Types/Node.cs
index c0a4ece..8c364fe 100644
--- a/src/Blazor.SourceGenerators/TypeScript/Types/Node.cs
+++ b/src/Blazor.SourceGenerators/TypeScript/Types/Node.cs
@@ -2,14 +2,12 @@
// 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 Children { get; set; } = new();
@@ -17,9 +15,14 @@ public class Node : TextRange, INode
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; }
@@ -63,23 +66,27 @@ public void ParseChildren(ITypeScriptAbstractSyntaxTree abstractSyntaxTree) =>
return null;
});
- public string GetText(string source = null)
+ public ReadOnlySpan 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 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);
diff --git a/src/Blazor.SourceGenerators/Types/DependentTypeMapper.cs b/src/Blazor.SourceGenerators/Types/DependentTypeMapper.cs
new file mode 100644
index 0000000..ee55040
--- /dev/null
+++ b/src/Blazor.SourceGenerators/Types/DependentTypeMapper.cs
@@ -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 s_lazyDefaultAst = new(() =>
+ {
+ var reader = TypeDeclarationReader.Default;
+ return TypeScriptAbstractSyntaxTree.FromSourceText(reader.RawSourceText);
+ });
+
+ ///
+ /// Gets a where the TKey is a
+ /// and the TValue is a .
+ ///
+ /// The name of the Name in the TypeScript
+ /// _.d.ts_ file's interface name { ... } definition.
+ /// A representation of all the dependent types as a
+ /// where the TKey is a
+ /// and the TValue is a .
+ /// Returns empty when the underlying AST parser is null or has no
+ /// root node, or if there isn't an interface found.
+ ///
+ public static Dictionary 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();
+ }
+
+ var dependentTypes = new Dictionary
+ {
+ [interfaceNode.Identifier] = interfaceNode
+ };
+
+ var queue = new Queue();
+ queue.Enqueue(interfaceNode);
+
+ while (queue.Count > 0)
+ {
+ var node = queue.Dequeue();
+
+ // Get methods
+ var methods =
+ node.OfKind(TypeScriptSyntaxKind.MethodSignature).Cast();
+
+ foreach (var method in methods)
+ {
+ // Get method parameters
+ var parameters =
+ method.OfKind(TypeScriptSyntaxKind.Parameter).Cast();
+
+ foreach (var parameter in parameters)
+ {
+ // Get parameter type
+ var typeReferences =
+ parameter.OfKind(TypeScriptSyntaxKind.TypeReference)
+ .Cast();
+
+ 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();
+
+ foreach (var typeReference in returnTypeReferences)
+ {
+ dependentTypes[typeReference.Identifier] = typeReference;
+ queue.Enqueue(typeReference);
+ }
+ }
+
+ // Get properties
+ var properties =
+ node.OfKind(TypeScriptSyntaxKind.PropertySignature)
+ .Cast();
+
+ foreach (var property in properties)
+ {
+ dependentTypes[property.Identifier] = property;
+ queue.Enqueue(property);
+ }
+
+ foreach (var childNode in node.Children)
+ {
+ queue.Enqueue(childNode);
+ }
+ }
+
+ return dependentTypes;
+ }
+}
diff --git a/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs b/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs
index 8cf9933..4bdf6c4 100644
--- a/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs
+++ b/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs
@@ -16,7 +16,7 @@ public class LibDomParserTests
public LibDomParserTests()
{
var reader = TypeDeclarationReader.Default;
- _sut = new TypeScriptAbstractSyntaxTree(reader.RawSourceText);
+ _sut = TypeScriptAbstractSyntaxTree.FromSourceText(reader.RawSourceText);
}
[Fact]
@@ -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();
diff --git a/tests/Blazor.SourceGenerators.Tests/TypeMapperTests.cs b/tests/Blazor.SourceGenerators.Tests/TypeMapperTests.cs
new file mode 100644
index 0000000..8ebff4f
--- /dev/null
+++ b/tests/Blazor.SourceGenerators.Tests/TypeMapperTests.cs
@@ -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 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"]);
+ }
+}