@@ -148,7 +148,7 @@ public static void Generate(LuaDocumentation docs, string path)
148148 else
149149 {
150150 sb . Append ( $ "---@param { parameter . Name } ") ;
151- if ( parameter . IsOptional || IsNullable ( parameter . ParameterType ) )
151+ if ( CanBeNil ( parameter ) )
152152 {
153153 sb . Append ( '?' ) ;
154154 }
@@ -170,7 +170,13 @@ public static void Generate(LuaDocumentation docs, string path)
170170 if ( func . Method . ReturnType != typeof ( void ) )
171171 {
172172 sb . Append ( "---@return " ) ;
173- sb . Append ( GetLuaType ( func . Method . ReturnParameter ) ) ;
173+ var luaType = GetLuaType ( func . Method . ReturnParameter ) ;
174+ var nilable = CanBeNil ( func . Method . ReturnParameter ) ;
175+ var wrapType = nilable && luaType . IndexOfAny ( [ ':' , '|' ] ) != - 1 ; // ? is ambiguous on complex types like `string|int` or `fun(): string`
176+ if ( wrapType ) sb . Append ( '(' ) ;
177+ sb . Append ( luaType ) ;
178+ if ( wrapType ) sb . Append ( ')' ) ;
179+ if ( nilable ) sb . Append ( '?' ) ;
174180 if ( IsZeroIndexed ( func . Method . ReturnParameter ) )
175181 {
176182 sb . Append ( " # Zero-indexed array." ) ;
@@ -261,7 +267,7 @@ private static string GetLuaType(Type type)
261267 return GetLuaType ( type . GetElementType ( ) ) + "[]" ;
262268 }
263269
264- if ( IsNullable ( type ) )
270+ if ( IsNullableValueType ( type ) )
265271 {
266272 type = type . GetGenericArguments ( ) [ 0 ] ;
267273 }
@@ -274,7 +280,48 @@ private static string GetLuaType(Type type)
274280 throw new NotSupportedException ( $ "Unknown type { type . FullName } used in API. Generator must be updated to handle this.") ;
275281 }
276282
277- private static bool IsNullable ( Type type ) => type . IsGenericType && type . GetGenericTypeDefinition ( ) == typeof ( Nullable < > ) ;
283+ private static bool CanBeNil ( ParameterInfo parameter ) => parameter . HasDefaultValue || IsNullableValueType ( parameter . ParameterType ) || IsNullableReferenceType ( parameter ) ;
284+
285+ private static bool IsNullableValueType ( Type type ) => type . IsValueType && type . IsGenericType && type . GetGenericTypeDefinition ( ) == typeof ( Nullable < > ) ;
286+
287+ /// <summary>
288+ /// Returns <see langword="true"/> if <paramref name="parameter"/> is a reference type and is annotated as nullable or lacks NRT annotations.
289+ /// </summary>
290+ /// <remarks>
291+ /// Only handles "top-level" types, not array elements or generic type parameters.
292+ /// </remarks>
293+ private static bool IsNullableReferenceType ( ParameterInfo parameter )
294+ {
295+ if ( parameter . ParameterType . IsValueType ) return false ;
296+
297+ // https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md
298+ const byte AnnotatedNotNull = 1 ;
299+
300+ // Check [Nullable] on the parameter first
301+ if ( GetNullableFlags ( parameter ) is byte [ ] flags )
302+ {
303+ return flags [ 0 ] != AnnotatedNotNull ;
304+ }
305+
306+ // Check [NullableContext] on the method and parent types
307+ var parent = parameter . Member ;
308+ while ( parent is not null )
309+ {
310+ if ( GetNullableContext ( parent ) is byte flag )
311+ {
312+ return flag != AnnotatedNotNull ;
313+ }
314+ parent = parent . DeclaringType ;
315+ }
316+
317+ return true ;
318+
319+ // Attributes may be compiled into each assembly, so can't be strongly typed
320+ static byte [ ] ? GetNullableFlags ( ParameterInfo parameter ) =>
321+ ( ( dynamic ) parameter . GetCustomAttributes ( ) . SingleOrDefault ( attr => attr . GetType ( ) . FullName == "System.Runtime.CompilerServices.NullableAttribute" ) ) ? . NullableFlags ;
322+ static byte ? GetNullableContext ( MemberInfo member ) =>
323+ ( ( dynamic ) member . GetCustomAttributes ( ) . SingleOrDefault ( attr => attr . GetType ( ) . FullName == "System.Runtime.CompilerServices.NullableContextAttribute" ) ) ? . Flag ;
324+ }
278325
279326 private static bool IsParams ( ParameterInfo parameter ) => parameter . GetCustomAttribute < ParamArrayAttribute > ( ) is not null ;
280327
0 commit comments