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"]); + } +}