xamarin-macios/tests/xtro-sharpie/ObjCProtocolCheck.cs

241 строка
6.9 KiB
C#
Исходник Обычный вид История

//
2016-05-26 16:06:52 +03:00
// The rule reports
//
// !missing-protocol!
// if headers defines a protocol that we have not bound as [Protocol]
//
// !incorrect-protocol-member!
// if we have @required members without [Abstract] or @optional members with [Abstract]
//
// !missing-protocol-member!
// if we have protocol members (found in header files) but not in the interface
//
// !extra-protocol-member!
// if we have protocol members in the interface that are NOT found in header files
//
// Limitations
//
// * .NET interfaces does not allow constructors, so we cannot check for `init*` members
//
// * .NET interfaces cannot have static members, so we cannot check for [Static] members
//
// Notes: Both limitations could be _mostly_ lifted by another tests that would check types conformance to a protocol
//
using System;
using System.Collections.Generic;
using System.Text;
using Mono.Cecil;
using Clang.Ast;
namespace Extrospection {
2022-11-08 19:44:13 +03:00
2016-05-26 16:06:52 +03:00
public class ObjCProtocolCheck : BaseVisitor {
Dictionary<string, TypeDefinition> protocol_map = new Dictionary<string, TypeDefinition> ();
public override void VisitManagedType (TypeDefinition type)
{
if (!type.HasCustomAttributes)
return;
if (!type.IsInterface) {
// Only interfaces map to protocols, but unfortunately we add [Protocol] to generated model classes too, so we need to skip those.
return;
}
2016-05-26 16:06:52 +03:00
string pname = null;
bool informal = false;
foreach (var ca in type.CustomAttributes) {
switch (ca.Constructor.DeclaringType.Name) {
case "ProtocolAttribute":
if (!ca.HasProperties)
continue;
foreach (var p in ca.Properties) {
switch (p.Name) {
case "Name":
pname = p.Argument.Value as string;
break;
case "IsInformal":
informal = (bool) p.Argument.Value;
break;
}
}
break;
}
}
if (!informal && !String.IsNullOrEmpty (pname))
protocol_map.Add (pname, type);
}
public override void VisitObjCProtocolDecl (ObjCProtocolDecl decl, VisitKind visitKind)
{
if (visitKind != VisitKind.Enter)
return;
if (!decl.IsDefinition)
return;
// check availability macros to see if the API is available on the OS and not deprecated
if (!decl.IsAvailable ())
return;
var framework = Helpers.GetFramework (decl);
if (framework is null)
return;
2016-05-26 16:06:52 +03:00
var name = decl.Name;
TypeDefinition td;
if (!protocol_map.TryGetValue (name, out td)) {
if (!decl.IsDeprecated ())
Log.On (framework).Add ($"!missing-protocol! {name} not bound");
2016-05-26 16:06:52 +03:00
// other checks can't be done without an actual protocol to inspect
return;
}
// build type selector-required map
var map = new Dictionary<string, bool> ();
foreach (var ca in td.CustomAttributes) {
string export = null;
string g_export = null;
string s_export = null;
bool is_required = false;
bool is_property = false;
bool is_static = false;
2016-05-26 16:06:52 +03:00
switch (ca.Constructor.DeclaringType.Name) {
case "ProtocolMemberAttribute":
foreach (var p in ca.Properties) {
switch (p.Name) {
case "Selector":
export = p.Argument.Value as string;
break;
case "GetterSelector":
g_export = p.Argument.Value as string;
break;
case "SetterSelector":
s_export = p.Argument.Value as string;
break;
case "IsRequired":
2022-11-08 19:44:13 +03:00
is_required = (bool) p.Argument.Value;
2016-05-26 16:06:52 +03:00
break;
case "IsProperty":
2022-11-08 19:44:13 +03:00
is_property = (bool) p.Argument.Value;
2016-05-26 16:06:52 +03:00
break;
case "IsStatic":
2022-11-08 19:44:13 +03:00
is_static = (bool) p.Argument.Value;
break;
2016-05-26 16:06:52 +03:00
}
}
break;
}
if (is_property) {
if (g_export is not null) {
if (is_static)
g_export = "+" + g_export;
2016-05-26 16:06:52 +03:00
map.Add (g_export, is_required);
}
if (s_export is not null) {
if (is_static)
s_export = "+" + s_export;
2016-05-26 16:06:52 +03:00
map.Add (s_export, is_required);
}
} else if (export is not null) {
if (is_static)
export = "+" + export;
map.Add (export, is_required);
2016-05-26 16:06:52 +03:00
}
}
var deprecatedProtocol = (decl.DeclContext as Decl).IsDeprecated ();
// don't report anything for deprecated protocols
// (we still report some errors for deprecated members of non-deprecated protocols - because abstract/non-abstract can
// change the managed API and we want to get things right, even if for deprecated members).
if (!deprecatedProtocol) {
var remaining = new Dictionary<string, bool> (map);
// check that required members match the [Abstract] members
foreach (ObjCMethodDecl method in decl.Methods) {
// some members might not be part of the current platform
if (!method.IsAvailable ())
continue;
var selector = GetSelector (method);
if (selector is null)
continue;
// a .NET interface cannot have constructors - so we cannot enforce that on the interface
if (IsInit (selector))
continue;
if (method.IsClassMethod)
selector = "+" + selector;
bool is_abstract;
if (map.TryGetValue (selector, out is_abstract)) {
bool required = method.ImplementationControl == ObjCImplementationControl.Required;
if (required) {
if (!is_abstract)
Log.On (framework).Add ($"!incorrect-protocol-member! {GetName (decl, method)} is REQUIRED and should be abstract");
} else {
if (is_abstract)
Log.On (framework).Add ($"!incorrect-protocol-member! {GetName (decl, method)} is OPTIONAL and should NOT be abstract");
}
remaining.Remove (selector);
} else if (!method.IsClassMethod) {
// a .NET interface cannot have static methods - so we can only report missing instance methods
if (!decl.IsDeprecated ())
Log.On (framework).Add ($"!missing-protocol-member! {GetName (decl, method)} not found");
remaining.Remove (selector);
2016-05-26 16:06:52 +03:00
}
}
foreach (var selector in remaining.Keys)
Log.On (framework).Add ($"!extra-protocol-member! unexpected selector {decl.Name}::{selector} found");
remaining.Clear ();
}
2016-05-26 16:06:52 +03:00
map.Clear ();
protocol_map.Remove (name);
}
static string GetSelector (ObjCMethodDecl method)
{
var result = method.Selector.ToString ();
if (result is not null)
2016-05-26 16:06:52 +03:00
return result;
if (method.IsPropertyAccessor || (method.DeclContext is ObjCProtocolDecl))
return method.Name;
return null;
}
static string GetName (ObjCProtocolDecl decl, ObjCMethodDecl method)
{
var sb = new StringBuilder ();
if (method.IsClassMethod)
sb.Append ('+');
sb.Append (decl.Name);
sb.Append ("::");
sb.Append (GetSelector (method));
return sb.ToString ();
}
bool IsInit (string selector)
{
return selector.StartsWith ("init", StringComparison.Ordinal) && Char.IsUpper (selector [4]);
}
public override void End ()
{
// at this stage anything else we have is not something we could find in Apple's headers
foreach (var kvp in protocol_map) {
var extra = kvp.Key;
var fx = kvp.Value.Namespace;
Log.On (fx).Add ($"!unknown-protocol! {extra} bound");
2016-05-26 16:06:52 +03:00
}
}
}
}