Skip to content

Commit

Permalink
JSExport improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin committed Apr 11, 2024
1 parent 947f4c1 commit aec2cbe
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 68 deletions.
53 changes: 45 additions & 8 deletions src/NodeApi.Generator/ModuleGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private IEnumerable<ITypeSymbol> GetCompilationTypes()
ReportError(
DiagnosticId.InvalidModuleInitializer,
type,
"[JSModule] attribute must be applied to a class.");
"[JSModule] attribute must be applied to a class or method.");
}
else if (type.DeclaredAccessibility != Accessibility.Public)
{
Expand All @@ -140,7 +140,7 @@ private IEnumerable<ITypeSymbol> GetCompilationTypes()
ReportError(
DiagnosticId.InvalidModuleInitializer,
member,
"[JSModule] attribute must be applied to a method.");
"[JSModule] attribute must be applied to a class or method.");
}
else if (!member.IsStatic)
{
Expand Down Expand Up @@ -194,15 +194,15 @@ private IEnumerable<ISymbol> GetModuleExportItems()
{
foreach (ITypeSymbol type in GetCompilationTypes())
{
if (type.GetAttributes().Any((a) => a.AttributeClass?.Name == "JSExportAttribute"))
if (IsExported(type))
{
if (type.TypeKind != TypeKind.Class &&
type.TypeKind != TypeKind.Struct &&
type.TypeKind != TypeKind.Interface &&
type.TypeKind != TypeKind.Delegate &&
type.TypeKind != TypeKind.Enum)
{
ReportError(
ReportWarning(
DiagnosticId.UnsupportedTypeKind,
type,
$"Exporting {type.TypeKind} types is not supported.");
Expand All @@ -222,8 +222,7 @@ private IEnumerable<ISymbol> GetModuleExportItems()
{
foreach (ISymbol? member in type.GetMembers())
{
if (member.GetAttributes().Any(
(a) => a.AttributeClass?.Name == "JSExportAttribute"))
if (IsExported(member))
{
if (type.DeclaredAccessibility != Accessibility.Public)
{
Expand All @@ -239,7 +238,7 @@ private IEnumerable<ISymbol> GetModuleExportItems()
member,
"Exported member must be public.");
}
else if (!(member.IsStatic))
else if (!member.IsStatic)
{
ReportError(
DiagnosticId.ExportIsNotStatic,
Expand Down Expand Up @@ -279,6 +278,7 @@ private SourceBuilder GenerateModuleInitializer(
string generatorName = typeof(ModuleGenerator).Assembly.GetName()!.Name!;
Version? generatorVersion = typeof(ModuleGenerator).Assembly.GetName().Version;
s += $"[GeneratedCode(\"{generatorName}\", \"{generatorVersion}\")]";
s += "[JSExport(false)]"; // Prevent typedefs from being generated for this class.
s += $"public static class {ModuleInitializerClassName}";
s += "{";

Expand Down Expand Up @@ -712,6 +712,41 @@ private void ExportDelegate(ITypeSymbol delegateType)
_callbackAdapters.Add(toAapter.Name!, toAapter);
}

public static bool IsExported(ISymbol symbol)
{
AttributeData? exportAttribute = GetJSExportAttribute(symbol);

// A private symbol with no [JSExport] attribute is not exported.
if (exportAttribute == null && symbol.DeclaredAccessibility != Accessibility.Public)
{
return false;
}

// If the symbol doesn't have a [JSExport] attribute, check its containing type
// and containing assembly.
while (exportAttribute == null &&
symbol.ContainingType?.DeclaredAccessibility == Accessibility.Public)
{
symbol = symbol.ContainingType;
exportAttribute = GetJSExportAttribute(symbol);
}

if (exportAttribute == null)
{
exportAttribute = GetJSExportAttribute(symbol.ContainingAssembly);

if (exportAttribute == null)
{
return false;
}
}

// If the [JSExport] attribute has a single boolean constructor argument, use that.
// Any other constructor defaults to true.
TypedConstant constructorArgument = exportAttribute.ConstructorArguments.SingleOrDefault();
return constructorArgument.Value as bool? ?? true;
}

/// <summary>
/// Gets the projected name for a symbol, which may be different from its C# name.
/// </summary>
Expand All @@ -734,7 +769,9 @@ public static string GetExportName(ISymbol symbol)
public static AttributeData? GetJSExportAttribute(ISymbol symbol)
{
return symbol.GetAttributes().SingleOrDefault(
(a) => a.AttributeClass?.Name == "JSExportAttribute");
(a) => a.AttributeClass?.Name == typeof(JSExportAttribute).Name &&
a.AttributeClass.ContainingNamespace.ToDisplayString() ==
typeof(JSExportAttribute).Namespace);
}

/// <summary>
Expand Down
75 changes: 43 additions & 32 deletions src/NodeApi.Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,7 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
// Imports will be inserted here later, after the used references are determined.
int importsIndex = s.Length;

// Assume module while finding exported items. Then update the module status afterward.
_isModule = true;
_exportedMembers.AddRange(GetExportedMembers());
_isModule = _exportedMembers.Count > 0;

// Default to camel-case for modules, preserve case otherwise.
_autoCamelCase = autoCamelCase ?? _isModule;
Expand All @@ -425,7 +422,7 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)

foreach (Type type in _assembly.GetTypes().Where((t) => t.IsPublic))
{
if (IsTypeExported(type))
if (!_isModule || IsExported(type))
{
ExportType(ref s, type);
}
Expand All @@ -434,7 +431,7 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
foreach (MemberInfo member in type.GetMembers(
BindingFlags.Public | BindingFlags.Static))
{
if (IsMemberExported(member))
if (IsExported(member))
{
ExportMember(ref s, member);
}
Expand Down Expand Up @@ -541,9 +538,11 @@ public SourceText GenerateModuleLoader(ModuleType moduleType, bool isSystemAssem
return s;
}

private bool IsTypeExported(Type type)
private bool IsExported(MemberInfo member)
{
if (!(type.IsPublic || type.IsNestedPublic) || IsExcludedNamespace(type.Namespace))
Type type = member as Type ?? member.DeclaringType!;

if (IsExcludedNamespace(type.Namespace))
{
return false;
}
Expand All @@ -557,55 +556,67 @@ private bool IsTypeExported(Type type)
return false;
}

if (!_isModule || type.GetCustomAttributesData().Any((a) =>
a.AttributeType.FullName == typeof(JSModuleAttribute).FullName ||
a.AttributeType.FullName == typeof(JSExportAttribute).FullName))
CustomAttributeData? exportAttribute = GetAttribute<JSExportAttribute>(member);

// If the member doesn't have a [JSExport] attribute, check its declaring type
// and declaring assembly.
while (exportAttribute == null && member.DeclaringType != null &&
(member.DeclaringType.IsPublic || member.DeclaringType.IsNestedPublic))
{
return true;
member = member.DeclaringType;
exportAttribute = GetAttribute<JSExportAttribute>(member);
}

if (type.IsNested)
if (exportAttribute == null)
{
return IsTypeExported(type.DeclaringType!);
}
exportAttribute = type.Assembly.GetCustomAttributesData().FirstOrDefault((a) =>
a.AttributeType.FullName == typeof(JSExportAttribute).FullName);

return false;
}
if (exportAttribute == null)
{
return false;
}
}

private static bool IsMemberExported(MemberInfo member)
{
return member.GetCustomAttributesData().Any((a) =>
a.AttributeType.FullName == typeof(JSExportAttribute).FullName);
// If the [JSExport] attribute has a single boolean constructor argument, use that.
// Any other constructor defaults to true.
CustomAttributeTypedArgument constructorArgument =
exportAttribute.ConstructorArguments.SingleOrDefault();
return constructorArgument.Value as bool? ?? true;
}

private static bool IsCustomModuleInitMethod(MemberInfo member)
private static CustomAttributeData? GetAttribute<T>(MemberInfo member)
{
return member is MethodInfo && member.GetCustomAttributesData().Any((a) =>
a.AttributeType.FullName == typeof(JSModuleAttribute).FullName);
return member.GetCustomAttributesData().FirstOrDefault((a) =>
a.AttributeType.FullName == typeof(T).FullName);
}

private IEnumerable<MemberInfo> GetExportedMembers()
{
foreach (Type type in _assembly.GetTypes().Where((t) => t.IsPublic))
{
if (IsTypeExported(type))
if (GetAttribute<JSModuleAttribute>(type) != null)
{
_isModule = true;
}
else if (IsExported(type))
{
_isModule = true;
yield return type;
}
else
{
foreach (MemberInfo member in type.GetMembers(
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static))
{
if (IsMemberExported(member))
if (GetAttribute<JSModuleAttribute>(member) != null)
{
yield return member;
_isModule = true;
}
else if (IsCustomModuleInitMethod(member))
else if (IsExported(member))
{
throw new InvalidOperationException(
"Cannot generate type definitions for an assembly with a " +
"custom [JSModule] initialization method.");
_isModule = true;
yield return member;
}
}
}
Expand Down Expand Up @@ -1533,7 +1544,7 @@ private string GetTSType(
string tsTypeArg = GetTSType(typeArg, typeArgsNullability?[0], allowTypeParams);
tsType = $"(value: {tsTypeArg}) => boolean";
}
else if (IsTypeExported(type))
else if (!_isModule || IsExported(type))
{
// Types exported from a module are not namespaced.
string nsPrefix = !_isModule && type.Namespace != null ? type.Namespace + '.' : "";
Expand Down Expand Up @@ -1565,7 +1576,7 @@ private string GetTSType(
tsType = "Duplex";
_emitDuplex = true;
}
else if (IsTypeExported(type))
else if (!_isModule || IsExported(type))
{
// Types exported from a module are not namespaced.
string nsPrefix = !_isModule && type.Namespace != null ? type.Namespace + '.' : "";
Expand Down
Loading

0 comments on commit aec2cbe

Please sign in to comment.