Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSExportAttribute improvements #272

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 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 @@ -216,14 +216,18 @@ private IEnumerable<ISymbol> GetModuleExportItems()
"Exported type must be public.");
}

yield return type;
// Don't return nested types when the containing type is also exported.
// Nested types will be exported as properties of their containing type.
if (type.ContainingType == null || !IsExported(type.ContainingType))
{
yield return type;
}
}
else if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct)
{
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 +243,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 +283,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 +717,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 +774,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
Loading