diff --git a/src/NodeApi.Generator/ModuleGenerator.cs b/src/NodeApi.Generator/ModuleGenerator.cs index 7618d52a..43575c9b 100644 --- a/src/NodeApi.Generator/ModuleGenerator.cs +++ b/src/NodeApi.Generator/ModuleGenerator.cs @@ -113,7 +113,7 @@ private IEnumerable 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) { @@ -140,7 +140,7 @@ private IEnumerable 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) { @@ -194,7 +194,7 @@ private IEnumerable 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 && @@ -202,7 +202,7 @@ private IEnumerable GetModuleExportItems() type.TypeKind != TypeKind.Delegate && type.TypeKind != TypeKind.Enum) { - ReportError( + ReportWarning( DiagnosticId.UnsupportedTypeKind, type, $"Exporting {type.TypeKind} types is not supported."); @@ -216,14 +216,18 @@ private IEnumerable 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) { @@ -239,7 +243,7 @@ private IEnumerable GetModuleExportItems() member, "Exported member must be public."); } - else if (!(member.IsStatic)) + else if (!member.IsStatic) { ReportError( DiagnosticId.ExportIsNotStatic, @@ -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 += "{"; @@ -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; + } + /// /// Gets the projected name for a symbol, which may be different from its C# name. /// @@ -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); } /// diff --git a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs index d48c8426..2b92ff39 100644 --- a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs +++ b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs @@ -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; @@ -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); } @@ -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); } @@ -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; } @@ -557,39 +556,52 @@ 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(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(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(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 GetExportedMembers() { foreach (Type type in _assembly.GetTypes().Where((t) => t.IsPublic)) { - if (IsTypeExported(type)) + if (GetAttribute(type) != null) { + _isModule = true; + } + else if (IsExported(type)) + { + _isModule = true; yield return type; } else @@ -597,15 +609,14 @@ private IEnumerable GetExportedMembers() foreach (MemberInfo member in type.GetMembers( BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static)) { - if (IsMemberExported(member)) + if (GetAttribute(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; } } } @@ -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 + '.' : ""; @@ -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 + '.' : ""; diff --git a/src/NodeApi/JSExportAttribute.cs b/src/NodeApi/JSExportAttribute.cs index 37c457a2..4293363f 100644 --- a/src/NodeApi/JSExportAttribute.cs +++ b/src/NodeApi/JSExportAttribute.cs @@ -10,54 +10,91 @@ namespace Microsoft.JavaScript.NodeApi; /// it is the default export of the module. /// /// -/// A public static class tagged with is exported as a JavaScript -/// object, with public members of the static class automatically exported as properties on the -/// object. +/// When applied to an assembly, all public types in the assembly are exported, unless excluded +/// by another . When applied to a publci type, all public members +/// of the type are exported, unless excluded by another or +/// unsupported for JS export. +/// +/// A static class is exported as a JavaScript object, with public members of the static class +/// automatically exported as properties on the object. /// -/// A public non-static class or struct tagged with is exported as -/// a JavaScript class, with public static members automatically exported as properties of the -/// class constructor object, and public instance members automatically exported as properties of -/// the class. .NET classes are passed by reference, such that a JavaScript instance of the class -/// is always backed by a .NET instance; structs are passed by value. +/// A non-static class or struct is exported as a JavaScript class, with static members +/// automatically exported as properties of the class constructor object, and instance members +/// automatically exported as properties of the class. .NET classes are passed by reference, such +/// that a JavaScript instance of the class is always backed by a .NET instance; structs are passed +/// by value. /// -/// A public static property tagged with is exported as a JavaScript -/// property on the module object. +/// A static property exported without its containing class is exported as a JavaScript property +/// on the module object. (Note module-level properties are constant when using ES modules: their +/// value is only read once at module initialization time.) /// -/// A public static method tagged with is exported as a JavaScript +/// A static method exported without its containing class is exported as a JavaScript /// function property on the module object. /// -/// A public enum, interface, or delegate tagged with is exported -/// as part of the type definitions of the module but does not have any runtime representation in -/// the JavaScript exports. -/// -/// (This attribute may not be applied to non-static properties or methods, any fields, or any -/// non-public items.) +/// An enum, interface, or delegate is exported as part of the type definitions of the module but +/// does not have any runtime representation in the JavaScript exports. /// +/// +/// +/// // Export all public types in the current assembly (unless otherwise specified). +/// [assembly: JSExport] +/// +/// // This type is not exported. The type-level attribute overrides the assembly-level attribute. +/// [JSExport(false)] +/// public static class NonExportedClass +/// { +/// // This method is exported as a static method on the module (not on the unexported class). +/// // The member-level attribute overrides any class or assembly-level attributes. +/// [JSExport] +/// public static void ModuleMethod(); +/// } +/// +/// // Without a type-level attribute, public types are exported by the assembly-level attribute. +/// public class ExportedClass +/// { +/// // The member-level attribute overrides any class or assembly-level attributes. +/// [JSExport(false)] +/// public void NonExportedMethod() {} +/// +/// // Without a member-level attribute, public members are exported along with the type. +/// public void ExportedMethod() {} +/// } +/// +/// [AttributeUsage( + AttributeTargets.Assembly | + AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Delegate | + AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.Method | - AttributeTargets.Enum | - AttributeTargets.Interface | - AttributeTargets.Delegate + AttributeTargets.Event )] public class JSExportAttribute : Attribute { /// - /// Exports an item as a property of the module exports object, with the JavaScript property - /// name auto-generated by camel-casing the .NET name. + /// Exports an item to JavaScript, with an auto-generated JavaScript name. /// - public JSExportAttribute() + /// + /// By default, type names are unchanged while member names are camel-cased for JavaScript. + /// + public JSExportAttribute() : this(export: true) { } /// - /// Exports an item as a property of the module exports object, with an explicit JavaScript - /// property name. + /// Exports an item to JavaScript, with an explicit JavaScript name. /// /// Name of the item as exported to JavaScript. - public JSExportAttribute(string name) + /// + /// Names must be unique among a module's exports. Duplicates will result in a build error. + /// + /// Use the name "default" to create a default export. + /// + public JSExportAttribute(string name) : this(export: true) { if (string.IsNullOrEmpty(name)) { @@ -67,9 +104,25 @@ public JSExportAttribute(string name) Name = name; } + /// + /// Excludes or includes an item for export to JavaScript. + /// + /// True to export the item (default), or false to exclude it from + /// exports. + public JSExportAttribute(bool export) + { + Export = export; + } + + /// + /// Gets a value indicating whether the item is exported to JavaScript. + /// + public bool Export { get; } + /// /// Gets the name of item as exported to JavaScript, or null if the JavaScript name is - /// auto-generated by camel-casing the .NET name. + /// auto-generated. By default, type names are unchanged while member names are camel-cased + /// for JavaScript. /// /// /// Names must be unique among a module's exports. Duplicates will result in a build error. diff --git a/test/NodeApi.Test.csproj b/test/NodeApi.Test.csproj index 7340a201..4a663468 100644 --- a/test/NodeApi.Test.csproj +++ b/test/NodeApi.Test.csproj @@ -12,6 +12,7 @@ + diff --git a/test/TestCases/projects/Module.cs b/test/TestCases/projects/Module.cs index e153c015..8c80ccbb 100644 --- a/test/TestCases/projects/Module.cs +++ b/test/TestCases/projects/Module.cs @@ -6,7 +6,10 @@ using Microsoft.JavaScript.NodeApi; +[assembly: JSExport] + // Tests exporting top-level properties on the JS module. +[JSExport(false)] public static class ModuleProperties { [JSExport] @@ -19,7 +22,6 @@ public static class ModuleProperties public static string Method(string arg) => arg; } -[JSExport] public class ModuleClass { public ModuleClass(string value)