2021-08-11 11:06:46 +03:00
using System ;
2016-05-26 16:06:52 +03:00
using System.Collections.Generic ;
using Mono.Cecil ;
using Clang.Ast ;
namespace Extrospection {
class EnumCheck : BaseVisitor {
2021-11-22 20:51:12 +03:00
class ManagedValue {
public FieldDefinition Field ;
public EnumConstantDecl Decl ;
}
2016-05-26 16:06:52 +03:00
2017-12-15 22:08:09 +03:00
Dictionary < string , TypeDefinition > enums = new Dictionary < string , TypeDefinition > ( StringComparer . InvariantCultureIgnoreCase ) ;
2021-08-10 14:40:25 +03:00
Dictionary < string , TypeDefinition > obsoleted_enums = new Dictionary < string , TypeDefinition > ( ) ;
2021-11-22 20:51:12 +03:00
Dictionary < object , ManagedValue > managed_values = new Dictionary < object , ManagedValue > ( ) ;
Dictionary < object , ( string Name , EnumConstantDecl Decl ) > native_values = new Dictionary < object , ( string Name , EnumConstantDecl Decl ) > ( ) ;
2016-05-26 16:06:52 +03:00
public override void VisitManagedType ( TypeDefinition type )
{
// exclude non enum and nested enums, e.g. bunch of Selector enums in CTFont
if ( ! type . IsEnum | | type . IsNested )
return ;
2021-08-10 14:40:25 +03:00
var name = type . Name ;
2019-08-14 18:46:55 +03:00
// exclude obsolete enums, presumably we already know there's something wrong with them if they've been obsoleted.
2021-08-10 14:40:25 +03:00
if ( type . IsObsolete ( ) ) {
obsoleted_enums [ name ] = type ;
2019-08-14 18:46:55 +03:00
return ;
2021-08-10 14:40:25 +03:00
}
if ( AttributeHelpers . HasAnyObsoleted ( type ) ) {
obsoleted_enums [ name ] = type ;
return ;
}
2019-08-14 18:46:55 +03:00
2016-05-26 16:06:52 +03:00
// e.g. WatchKit.WKErrorCode and WebKit.WKErrorCode :-(
2017-12-15 22:08:09 +03:00
if ( ! enums . TryGetValue ( name , out var td ) )
2016-05-26 16:06:52 +03:00
enums . Add ( name , type ) ;
2017-12-28 16:51:34 +03:00
else {
var ( t1 , t2 ) = Helpers . Sort ( type , td ) ;
if ( t1 . Namespace . StartsWith ( "OpenTK." , StringComparison . Ordinal ) ) {
// OpenTK duplicate a lots of enums between it's versions
} else if ( t1 . IsNotPublic & & String . IsNullOrEmpty ( t1 . Namespace ) ) {
// ignore special, non exposed types
} else {
var framework = Helpers . GetFramework ( t1 ) ;
Log . On ( framework ) . Add ( $"!duplicate-type-name! {name} enum exists as both {t1.FullName} and {t2.FullName}" ) ;
}
2016-05-26 16:06:52 +03:00
}
}
public override void VisitEnumDecl ( EnumDecl decl , VisitKind visitKind )
{
if ( visitKind ! = VisitKind . Enter )
return ;
if ( ! decl . IsDefinition )
return ;
string name = decl . Name ;
if ( name = = null )
return ;
// check availability macros to see if the API is available on the OS and not deprecated
if ( ! decl . IsAvailable ( ) )
return ;
2017-12-15 22:08:09 +03:00
var framework = Helpers . GetFramework ( decl ) ;
if ( framework = = null )
return ;
2016-05-26 16:06:52 +03:00
var mname = Helpers . GetManagedName ( name ) ;
2021-08-10 14:40:25 +03:00
// If our enum is obsoleted, then don't process it.
if ( obsoleted_enums . ContainsKey ( mname ) )
return ;
2017-12-15 22:08:09 +03:00
if ( ! enums . TryGetValue ( mname , out var type ) ) {
2021-11-18 18:26:30 +03:00
if ( ! decl . IsDeprecated ( ) ) // don't report deprecated enums as unbound
Log . On ( framework ) . Add ( $"!missing-enum! {name} not bound" ) ;
2016-05-26 16:06:52 +03:00
return ;
} else
enums . Remove ( mname ) ;
int native_size = 4 ;
bool native = false ;
// FIXME: this can be simplified
switch ( decl . IntegerQualType . ToString ( ) ) {
case "NSInteger" :
case "NSUInteger" :
case "CFIndex" :
case "CFOptionFlags" :
2019-07-18 12:44:34 +03:00
case "AVAudioInteger" :
2016-05-26 16:06:52 +03:00
native_size = 8 ; // in managed code it's always the largest size
native = true ;
break ;
case "unsigned long" :
case "unsigned int" :
case "int32_t" :
case "uint32_t" :
case "int" :
case "GLint" :
case "GLuint" :
case "GLenum" :
case "SInt32" :
case "UInt32" :
case "OptionBits" : // UInt32
case "long" :
case "FourCharCode" :
2017-09-06 05:38:02 +03:00
case "OSStatus" :
2016-05-26 16:06:52 +03:00
break ;
case "int64_t" :
case "uint64_t" :
case "unsigned long long" :
2016-06-28 00:18:30 +03:00
case "CVOptionFlags" : // uint64_t
2016-05-26 16:06:52 +03:00
native_size = 8 ;
break ;
case "UInt16" :
case "int16_t" :
case "uint16_t" :
case "short" :
native_size = 2 ;
break ;
case "UInt8" :
case "int8_t" :
case "uint8_t" :
native_size = 1 ;
break ;
default :
throw new NotImplementedException ( decl . IntegerQualType . ToString ( ) ) ;
}
// check correct [Native] decoration
2021-11-18 18:26:30 +03:00
if ( ! decl . IsDeprecated ( ) ) {
if ( native ) {
if ( ! IsNative ( type ) )
Log . On ( framework ) . Add ( $"!missing-enum-native! {name}" ) ;
} else {
if ( IsNative ( type ) )
Log . On ( framework ) . Add ( $"!extra-enum-native! {name}" ) ;
}
2016-05-26 16:06:52 +03:00
}
int managed_size = 4 ;
2020-10-05 22:57:18 +03:00
bool signed = true ;
2016-05-26 16:06:52 +03:00
switch ( GetEnumUnderlyingType ( type ) . Name ) {
case "Byte" :
2020-10-05 22:57:18 +03:00
signed = false ;
managed_size = 1 ;
break ;
2016-05-26 16:06:52 +03:00
case "SByte" :
managed_size = 1 ;
break ;
case "Int16" :
2020-10-05 22:57:18 +03:00
managed_size = 2 ;
break ;
2016-05-26 16:06:52 +03:00
case "UInt16" :
2020-10-05 22:57:18 +03:00
signed = false ;
2016-05-26 16:06:52 +03:00
managed_size = 2 ;
break ;
case "Int32" :
2020-10-05 22:57:18 +03:00
break ;
2016-05-26 16:06:52 +03:00
case "UInt32" :
2020-10-05 22:57:18 +03:00
signed = false ;
2016-05-26 16:06:52 +03:00
break ;
case "Int64" :
2020-10-05 22:57:18 +03:00
managed_size = 8 ;
break ;
2016-05-26 16:06:52 +03:00
case "UInt64" :
2020-10-05 22:57:18 +03:00
signed = false ;
2016-05-26 16:06:52 +03:00
managed_size = 8 ;
break ;
default :
throw new NotImplementedException ( ) ;
}
2020-10-05 22:57:18 +03:00
2021-11-22 20:51:12 +03:00
native_values . Clear ( ) ;
managed_values . Clear ( ) ;
2020-10-05 22:57:18 +03:00
2021-11-22 20:51:12 +03:00
// collect all the native enum values
var nativeConstant = signed ? ( object ) 0L : ( object ) 0 UL ;
foreach ( var value in decl . Values ) {
if ( ( value . InitExpr ! = null ) & & value . InitExpr . EvaluateAsInt ( decl . AstContext , out var integer ) ) {
if ( signed ) {
nativeConstant = integer . SExtValue ;
} else {
nativeConstant = integer . ZExtValue ;
}
2020-10-05 22:57:18 +03:00
}
2021-11-22 20:51:12 +03:00
if ( native_values . TryGetValue ( nativeConstant , out var entry ) ) {
// the same constant might be used for multiple values - some deprecated and some not,
// only overwrite if the current value isn't deprecated
if ( ! value . IsDeprecated ( ) )
native_values [ nativeConstant ] = new ( value . ToString ( ) , value ) ;
} else {
native_values [ nativeConstant ] = new ( value . ToString ( ) , value ) ;
2020-10-05 22:57:18 +03:00
}
2021-11-22 20:51:12 +03:00
// assume, sequentially assigned (in case next `value.InitExpr` is null)
var t = nativeConstant . GetType ( ) ;
if ( signed ) {
nativeConstant = 1L + ( long ) nativeConstant ;
} else {
nativeConstant = 1 UL + ( ulong ) nativeConstant ;
}
}
2020-10-05 22:57:18 +03:00
2021-11-22 20:51:12 +03:00
// collect all the managed enum values
var fields = type . Fields ;
foreach ( var f in fields ) {
// skip special `value__`
if ( f . IsRuntimeSpecialName & & ! f . IsStatic )
continue ;
if ( f . IsObsolete ( ) )
continue ;
object managedValue ;
if ( signed ) {
managedValue = Convert . ToInt64 ( f . Constant ) ;
} else {
managedValue = Convert . ToUInt64 ( f . Constant ) ;
2020-10-05 22:57:18 +03:00
}
2021-11-22 20:51:12 +03:00
managed_values [ managedValue ] = new ManagedValue { Field = f } ;
}
foreach ( var kvp in native_values ) {
var value = kvp . Key ;
var valueDecl = kvp . Value . Decl ;
var valueName = kvp . Value . Name ;
if ( managed_values . TryGetValue ( value , out var entry ) ) {
entry . Decl = valueDecl ;
} else {
// only for unsigned (flags) native enums we allow all bits set on 32 bits (UInt32.MaxValue)
// to be equal to all bit set on 64 bits (UInt64.MaxValue) since the MaxValue differs between
// 32bits (e.g. watchOS) and 64bits (all others) platforms
if ( ! signed & & native & & ( ulong ) value = = UInt32 . MaxValue & & managed_values . Remove ( UInt64 . MaxValue ) )
2020-10-05 22:57:18 +03:00
continue ;
2021-11-22 20:51:12 +03:00
// couldn't find a matching managed enum value for the native enum value
// don't report deprecated native enum values (or if the native enum itself is deprecated) as missing
if ( ! valueDecl . IsDeprecated ( ) & & ! decl . IsDeprecated ( ) )
Log . On ( framework ) . Add ( $"!missing-enum-value! {type.Name} native value {valueName} = {value} not bound" ) ;
2020-10-05 22:57:18 +03:00
}
2021-11-22 20:51:12 +03:00
}
2020-10-05 22:57:18 +03:00
2021-11-22 20:51:12 +03:00
foreach ( var kvp in managed_values ) {
var value = kvp . Key ;
var valueField = kvp . Value . Field ;
var valueDecl = kvp . Value . Decl ;
var fieldName = valueField . Name ;
2020-10-05 22:57:18 +03:00
2021-11-22 20:51:12 +03:00
// A 0 might be a valid extra value sometimes
var isZero = signed ? ( long ) value = = 0 : ( ulong ) value = = 0 ;
if ( isZero & & IsExtraZeroValid ( type . Name , fieldName ) )
continue ;
2020-10-05 22:57:18 +03:00
2021-11-22 20:51:12 +03:00
if ( valueDecl is null ) {
// we have a managed enum value, but no corresponding native value
// this is only an issue if the managed enum value is available and not obsoleted
if ( valueField . IsAvailable ( ) & & ! valueField . IsObsolete ( ) )
Log . On ( framework ) . Add ( $"!extra-enum-value! Managed value {value} for {type.Name}.{fieldName} not found in native headers" ) ;
continue ;
2020-10-05 22:57:18 +03:00
}
2021-11-22 20:51:12 +03:00
if ( ! valueDecl . IsAvailable ( ) ) {
// if the native enum value isn't available, the managed one shouldn't be either
2021-11-26 09:22:58 +03:00
if ( valueField . IsAvailable ( ) & & ! IsErrorEnum ( type ) )
2021-11-22 20:51:12 +03:00
Log . On ( framework ) . Add ( $"!extra-enum-value! Managed value {value} for {type.Name}.{fieldName} is available for the current platform while the value in the native header is not" ) ;
continue ;
2020-10-05 22:57:18 +03:00
}
}
2021-11-18 18:26:30 +03:00
if ( native_size ! = managed_size & & ! decl . IsDeprecated ( ) )
2017-12-15 22:08:09 +03:00
Log . On ( framework ) . Add ( $"!wrong-enum-size! {name} managed {managed_size} vs native {native_size}" ) ;
2016-05-26 16:06:52 +03:00
}
2021-11-26 09:22:58 +03:00
static bool IsErrorEnum ( TypeDefinition type )
{
if ( ! type . IsEnum )
return false ;
if ( type . Name . EndsWith ( "Error" , StringComparison . Ordinal ) )
return true ;
if ( type . Name . EndsWith ( "ErrorCode" , StringComparison . Ordinal ) )
return true ;
if ( ! type . HasCustomAttributes )
return false ;
foreach ( var ca in type . CustomAttributes ) {
if ( ca . AttributeType . Name = = "ErrorDomainAttribute" )
return true ;
}
return false ;
}
2020-10-05 22:57:18 +03:00
static bool IsExtraZeroValid ( string typeName , string valueName )
{
switch ( valueName ) {
// we often add `None = 0` to flags, when none exists
case "None" :
return true ;
// `Ok = 0` and `Success = 0` are often added to errors enums
case "Ok" :
case "Success" :
if ( typeName . EndsWith ( "ErrorCode" , StringComparison . Ordinal ) )
return true ;
if ( typeName . EndsWith ( "Status" , StringComparison . Ordinal ) )
return true ;
break ;
// used in HealthKit for a default value
case "NotApplicable" :
return typeName . StartsWith ( "HK" , StringComparison . Ordinal ) ;
}
return false ;
}
2016-05-26 16:06:52 +03:00
static bool IsNative ( TypeDefinition type )
{
if ( ! type . HasCustomAttributes )
return false ;
foreach ( var ca in type . CustomAttributes ) {
if ( ca . Constructor . DeclaringType . Name = = "NativeAttribute" )
return true ;
}
return false ;
}
public static TypeReference GetEnumUnderlyingType ( TypeDefinition self )
{
var fields = self . Fields ;
for ( int i = 0 ; i < fields . Count ; i + + ) {
var field = fields [ i ] ;
if ( ! field . IsStatic )
return field . FieldType ;
}
throw new ArgumentException ( ) ;
}
public override void End ( )
{
// report any [Native] decorated enum for which we could not find a match in the header files
// e.g. a typo in the name
foreach ( var extra in enums ) {
2017-12-15 22:08:09 +03:00
var t = extra . Value ;
2021-08-10 14:40:25 +03:00
if ( obsoleted_enums . ContainsKey ( t . Name ) )
continue ;
2017-12-15 22:08:09 +03:00
if ( ! IsNative ( t ) )
continue ;
var framework = Helpers . GetFramework ( t ) ;
Log . On ( framework ) . Add ( $"!unknown-native-enum! {extra.Key} bound" ) ;
2016-05-26 16:06:52 +03:00
}
}
}
2021-08-11 11:06:46 +03:00
}