Skip to content

Commit

Permalink
Fix typedef generation for JS project tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin committed Sep 24, 2023
1 parent af94688 commit 9a9015a
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 21 deletions.
10 changes: 0 additions & 10 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,5 @@
<NetFramework>false</NetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="all" />
<!-- All projects need to be rebuilt if the version changes. -->
<Content Include="$(MSBuildThisFileDirectory)version.json" Link="version.json">
<CopyToOutputDirectory>DoNotCopy</CopyToOutputDirectory>
<Visible>false</Visible><!-- Hide from VS solution explorer -->
<Pack>false</Pack> <!--Exclude from NuGet packages -->
</Content>
</ItemGroup>

<Import Project="./rid.props" />
</Project>
14 changes: 14 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<Import Project="../Directory.Build.props" />

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="all" />
<!-- All projects need to be rebuilt if the version changes. -->
<Content Include="$(MSBuildThisFileDirectory)version.json" Link="version.json">
<CopyToOutputDirectory>DoNotCopy</CopyToOutputDirectory>
<Visible>false</Visible><!-- Hide from VS solution explorer -->
<Pack>false</Pack> <!--Exclude from NuGet packages -->
</Content>
</ItemGroup>

</Project>
109 changes: 98 additions & 11 deletions src/NodeApi.Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
else
{
foreach (MemberInfo member in type.GetMembers(
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static))
BindingFlags.Public | BindingFlags.Static))
{
if (IsMemberExported(member))
{
Expand Down Expand Up @@ -560,14 +560,14 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
if (type.IsClass)
{
foreach (PropertyInfo property in type.GetProperties(
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static))
BindingFlags.Public | BindingFlags.Static))
{
if (isFirstMember) isFirstMember = false; else s++;
ExportTypeMember(ref s, property);
}

foreach (MethodInfo method in type.GetMethods(
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static))
BindingFlags.Public | BindingFlags.Static))
{
if (!IsExcludedMethod(method))
{
Expand All @@ -584,25 +584,42 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
GenerateDocComments(ref s, type);

bool isStaticClass = type.IsAbstract && type.IsSealed && !isGenericTypeDefinition;
bool isStreamSubclass = type.BaseType?.FullName == typeof(Stream).FullName;
string classKind = type.IsInterface || type.IsGenericTypeDefinition ?
"interface" : isStaticClass ? "namespace" : "class";
string implementsKind = type.IsInterface || type.IsGenericTypeDefinition ?
"extends" : "implements";

string implements = string.Empty;

foreach (Type interfaceType in type.GetInterfaces())
Type[] interfaceTypes = type.GetInterfaces();
foreach (Type interfaceType in interfaceTypes)
{
string prefix = (implements.Length == 0 ?
(type.IsInterface ? " extends " : " implements ") : ", ");
string prefix = (implements.Length == 0 ? $" {implementsKind}" : ",") +
(interfaceTypes.Length > 1 ? "\n\t" : " ");

if (interfaceType == typeof(IDisposable))
if (isStreamSubclass &&
(interfaceType.Name == nameof(IDisposable) ||
interfaceType.Name == nameof(IAsyncDisposable)))
{
// Stream projections extend JS Duplex class which has different close semantics.
continue;
}
else if (interfaceType == typeof(IDisposable))
{
implements += prefix + nameof(IDisposable);
_emitDisposable = true;
}
else if (interfaceType.Namespace != typeof(IList<>).Namespace)
else if (interfaceType.Namespace != typeof(IList<>).Namespace &&
!HasExplicitInterfaceImplementations(type, interfaceType))
{
// Extending generic collection interfaces gets tricky because of the way
// those are projected to JS types. For now, those are just omitted here.

// If any of the class's interface methods are implemented explicitly,
// the interface is omitted. TypeScript does not and cannot support
// explicit interface implementations because it uses duck typing.

string tsType = GetTSType(interfaceType, nullability: null);
if (tsType != "unknown")
{
Expand All @@ -611,7 +628,6 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
}
}

bool isStreamSubclass = type.BaseType?.FullName == typeof(Stream).FullName;
if (isStreamSubclass)
{
implements = " extends Duplex" + implements;
Expand All @@ -635,15 +651,17 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
if (!isStreamSubclass)
{
foreach (PropertyInfo property in type.GetProperties(
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance |
BindingFlags.Public | BindingFlags.Instance |
(isStaticClass ? BindingFlags.DeclaredOnly : default) |
(type.IsInterface || isGenericTypeDefinition ? default : BindingFlags.Static)))
{
if (isFirstMember) isFirstMember = false; else s++;
ExportTypeMember(ref s, property);
}

foreach (MethodInfo method in type.GetMethods(
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance |
BindingFlags.Public | BindingFlags.Instance |
(isStaticClass ? BindingFlags.DeclaredOnly : default) |
(type.IsInterface || isGenericTypeDefinition ? default : BindingFlags.Static)))
{
if (!IsExcludedMethod(method))
Expand All @@ -664,6 +682,69 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
}
}

private static bool HasExplicitInterfaceImplementations(Type type, Type interfaceType)
{
if (!type.IsClass)
{
if ((interfaceType.Name == nameof(IComparable) && type.IsInterface &&
type.GetInterfaces().Any((i) => i.Name == typeof(IComparable<>).Name)) ||
(interfaceType.Name == "ISpanFormattable" && type.IsInterface &&
type.GetInterfaces().Any((i) => i.Name == "INumberBase`1")))
{
// TS interfaces cannot extend multiple interfaces that have non-identical methods
// with the same name. This is most commonly an issue with IComparable and
// ISpanFormattable/INumberBase generic and non-generic interfaces.
return true;
}

return false;
}
else if (type.Name == "TypeDelegator" && interfaceType.Name == "IReflectableType")
{
// Special case: TypeDelegator has an explicit implementation of this interface,
// but it isn't detected by reflection due to the runtime type delegation.
return true;
}

// Note the InterfaceMapping class is not supported for assemblies loaded by a
// MetadataLoadContext, so the answer is a little harder to find.

if (interfaceType.IsConstructedGenericType)
{
interfaceType = interfaceType.GetGenericTypeDefinition();
}

// Get the interface type name with generic type parameters for matching.
// It would be more precise to match the generic type params also,
// but also more complicated.
string interfaceTypeName = interfaceType.FullName!;
int genericMarkerIndex = interfaceTypeName.IndexOf('`');
if (genericMarkerIndex >= 0)
{
interfaceTypeName = interfaceTypeName.Substring(0, genericMarkerIndex);
}

foreach (MethodInfo method in type.GetMethods(
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (method.IsFinal && method.IsPrivate &&
method.Name.StartsWith(interfaceTypeName))
{
return true;
}
}

foreach (Type baseInterfaceType in interfaceType.GetInterfaces())
{
if (HasExplicitInterfaceImplementations(type, baseInterfaceType))
{
return true;
}
}

return false;
}

private void GenerateGenericTypeFactory(ref SourceBuilder s, Type type)
{
GenerateDocComments(ref s, type, "[Generic type factory] ");
Expand Down Expand Up @@ -747,6 +828,12 @@ private void ExportTypeMember(ref SourceBuilder s, MemberInfo member)
string parameters = GetTSParameters(method.GetParameters());
string returnType = GetTSType(method.ReturnParameter);

if (methodName == nameof(IDisposable.Dispose))
{
// Match JS disposable naming convention.
methodName = "dispose";
}

if (declaringType.IsAbstract && declaringType.IsSealed &&
!declaringType.IsGenericTypeDefinition)
{
Expand Down

0 comments on commit 9a9015a

Please sign in to comment.