diff --git a/src/Uno.Analyzers.Tests/UnoNotImplementedTests.cs b/src/Uno.Analyzers.Tests/UnoNotImplementedTests.cs index 53b09a298429..d504045ceb74 100644 --- a/src/Uno.Analyzers.Tests/UnoNotImplementedTests.cs +++ b/src/Uno.Analyzers.Tests/UnoNotImplementedTests.cs @@ -10,6 +10,23 @@ namespace Uno.Analyzers.Tests [TestClass] public class UnoNotImplementedTests : DiagnosticVerifier { + private static string UnoNotImplementedAtribute = @" + namespace Uno + { + [System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)] + public sealed class NotImplementedAttribute : Attribute + { + public NotImplementedAttribute() { } + + public NotImplementedAttribute(params string[] platforms) + { + Platforms = platforms; + } + + public string[]? Platforms { get; } + } + }"; + protected override DiagnosticAnalyzer DiagnosticAnalyzer => new UnoNotImplementedAnalyzer(); public UnoNotImplementedTests() : base(LanguageNames.CSharp) @@ -25,7 +42,7 @@ public void Nothing() } [TestMethod] - public void When_LambdaIsAsyncVoid() + public void When_EmptyNotImplemented() { var test = @" using System; @@ -37,12 +54,6 @@ public void When_LambdaIsAsyncVoid() namespace Uno { - [System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)] - public sealed class NotImplementedAttribute : Attribute - { - - } - [NotImplemented] public class TestClass { } } @@ -56,7 +67,49 @@ public TypeName() var a = new Uno.TestClass(); } } - }"; + } + + " + UnoNotImplementedAtribute; + + var expected = new DiagnosticResult + { + Id = UnoNotImplementedAnalyzer.Rule.Id, + Severity = DiagnosticSeverity.Warning, + Message = string.Format(UnoNotImplementedAnalyzer.MessageFormat, "Uno.TestClass"), + Locations = new[] { + new DiagnosticResultLocation("Test0.cs", 21, 36) + } + }; + + VerifyDiagnostic(test, expected); + } + + [TestMethod] + public void When_SinglePlatform_Included() + { + var test = @" + #define __WASM__ + + using System; + + namespace Uno + { + [NotImplemented(""__WASM__"")] + public class TestClass { } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass(); + } + } + } + + " + UnoNotImplementedAtribute; var expected = new DiagnosticResult { @@ -64,11 +117,246 @@ public TypeName() Severity = DiagnosticSeverity.Warning, Message = string.Format(UnoNotImplementedAnalyzer.MessageFormat, "Uno.TestClass"), Locations = new[] { - new DiagnosticResultLocation("Test0.cs", 27, 36) + new DiagnosticResultLocation("Test0.cs", 18, 36) } }; VerifyDiagnostic(test, expected); } + + [TestMethod] + public void When_SinglePlatform_Excluded() + { + var test = @" + #define __WASM__ + + using System; + + namespace Uno + { + [NotImplemented(""__SKIA__"")] + public class TestClass { } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass(); + } + } + } + + " + UnoNotImplementedAtribute; + + VerifyDiagnostic(test); + } + + [TestMethod] + public void When_TwoPlatforms_Excluded() + { + var test = @" + #define __WASM__ + + using System; + + namespace Uno + { + [NotImplemented(""__SKIA__"", ""__IOS__"")] + public class TestClass { } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass(); + } + } + } + + " + UnoNotImplementedAtribute; + + VerifyDiagnostic(test); + } + + [TestMethod] + public void When_Generic_Excluded() + { + var test = @" + #define UNO_REFERENCE_API + + using System; + + namespace Uno + { + [NotImplemented(""__IOS__"")] + public class TestClass { } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass(); + } + } + } + + " + UnoNotImplementedAtribute; + + VerifyDiagnostic(test); + } + + [TestMethod] + public void When_Generic_Partial_Excluded() + { + var test = @" + #define UNO_REFERENCE_API + + using System; + + namespace Uno + { + [NotImplemented(""__SKIA__"", ""__IOS__"")] + public class TestClass { } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass(); + } + } + } + + " + UnoNotImplementedAtribute; + + VerifyDiagnostic(test); + } + + [TestMethod] + public void When_Generic_Included() + { + var test = @" + #define UNO_REFERENCE_API + + using System; + + namespace Uno + { + [NotImplemented(""__SKIA__"", ""__IOS__"", ""__WASM__"")] + public class TestClass { } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass(); + } + } + } + + " + UnoNotImplementedAtribute; + + var expected = new DiagnosticResult + { + Id = UnoNotImplementedAnalyzer.Rule.Id, + Severity = DiagnosticSeverity.Warning, + Message = string.Format(UnoNotImplementedAnalyzer.MessageFormat, "Uno.TestClass"), + Locations = new[] { + new DiagnosticResultLocation("Test0.cs", 18, 36) + } + }; + + VerifyDiagnostic(test, expected); + } + + + [TestMethod] + public void When_Generic_Member_Included() + { + var test = @" + #define UNO_REFERENCE_API + + using System; + + namespace Uno + { + public class TestClass { + [NotImplemented(""__SKIA__"", ""__IOS__"", ""__WASM__"")] + public int Test { get; } + } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass().Test; + } + } + } + + " + UnoNotImplementedAtribute; + + var expected = new DiagnosticResult + { + Id = UnoNotImplementedAnalyzer.Rule.Id, + Severity = DiagnosticSeverity.Warning, + Message = string.Format(UnoNotImplementedAnalyzer.MessageFormat, "Uno.TestClass.Test"), + Locations = new[] { + new DiagnosticResultLocation("Test0.cs", 20, 36) + } + }; + + VerifyDiagnostic(test, expected); + } + + [TestMethod] + public void When_Generic_Member_Partial_Excluded() + { + var test = @" + #define UNO_REFERENCE_API + + using System; + + namespace Uno + { + public class TestClass { + [NotImplemented(""__IOS__"", ""__WASM__"")] + public int Test { get; } + } + } + + namespace ConsoleApplication1 + { + class TypeName + { + public TypeName() + { + var a = new Uno.TestClass().Test; + } + } + } + + " + UnoNotImplementedAtribute; + + VerifyDiagnostic(test); + } } } diff --git a/src/Uno.Analyzers/UnoNotImplementedAnalyzer.cs b/src/Uno.Analyzers/UnoNotImplementedAnalyzer.cs index 294e6b09bb72..ee5d242ef1bd 100644 --- a/src/Uno.Analyzers/UnoNotImplementedAnalyzer.cs +++ b/src/Uno.Analyzers/UnoNotImplementedAnalyzer.cs @@ -1,6 +1,9 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -64,8 +67,9 @@ private void OnObjectCreationExpression(SyntaxNodeAnalysisContext contextAnalysi if (namedSymbol != null && IsUnoSymbol(symbol)) { + var directives = GetDirectives(contextAnalysis); - if (HasNotImplementedAttribute(notImplementedSymbol, namedSymbol)) + if (HasNotImplementedAttribute(notImplementedSymbol, namedSymbol, directives)) { var diagnostic = Diagnostic.Create( SupportedDiagnostics.First(), @@ -78,9 +82,67 @@ private void OnObjectCreationExpression(SyntaxNodeAnalysisContext contextAnalysi } } - private static bool HasNotImplementedAttribute(INamedTypeSymbol notImplementedSymbol, ISymbol namedSymbol) + private string[] GetDirectives(SyntaxNodeAnalysisContext contextAnalysis) { - return namedSymbol.GetAttributes().Any(a => a.AttributeClass == notImplementedSymbol); + var directives = contextAnalysis.Node.GetLocation()?.SourceTree.Options.PreprocessorSymbolNames.ToArray() ?? new string[0]; + + if (directives.Length == 0) + { + // This case is only used during tests where explicit #define statements are + // present at the top of the file. In common cases, PreprocessorSymbolNames is + // not empty. + + var directive = contextAnalysis + .Node + .GetLocation() + ?.SourceTree + .GetRoot() + .GetFirstDirective() as DefineDirectiveTriviaSyntax; + + if (directive != null) + { + directives = new[] { directive.Name.Text }; + } + } + + return directives; + } + + private static bool HasNotImplementedAttribute(INamedTypeSymbol notImplementedSymbol, ISymbol namedSymbol, string[] directives) + { + if(namedSymbol.GetAttributes().FirstOrDefault(a => Equals(a.AttributeClass, notImplementedSymbol)) is AttributeData data) + { + if ( + data.ConstructorArguments.FirstOrDefault() is TypedConstant constant + && constant.Kind != TypedConstantKind.Error) + { + Debug.Assert(constant.Kind == TypedConstantKind.Array); + + var notImplementedPlatforms = constant.Values.Select(v => v.Value?.ToString()).ToArray(); + + if (directives.Contains("UNO_REFERENCE_API") + && !directives.Contains("__SKIA__") + && !directives.Contains("__WASM__")) + { + // Uno reference API is a special case where if a member or symbol + // is implementer for either __SKIA__ or __WASM__, the member is considered + // implemented. The code may be running in either environments, and we cannot + // statically determine if a member will be available. + return notImplementedPlatforms.Any(p => p == "__SKIA__") + && notImplementedPlatforms.Any(p => p == "__WASM__"); + } + else + { + return notImplementedPlatforms.Any(d => directives.Contains(d)); + } + } + else + { + return true; + } + } + + return false; } private void OnMemberAccessExpression(SyntaxNodeAnalysisContext contextAnalysis, INamedTypeSymbol notImplementedSymbol) @@ -96,7 +158,12 @@ private void OnMemberAccessExpression(SyntaxNodeAnalysisContext contextAnalysis, if (member.Symbol != null && IsUnoSymbol(member)) { - if (HasNotImplementedAttribute(notImplementedSymbol, member.Symbol) || HasNotImplementedAttribute(notImplementedSymbol, member.Symbol.ContainingSymbol)) + var directives = GetDirectives(contextAnalysis); + + var isMemberNotImplemented = HasNotImplementedAttribute(notImplementedSymbol, member.Symbol, directives); + var isMemberOwnerNotImplemented = HasNotImplementedAttribute(notImplementedSymbol, member.Symbol.ContainingSymbol, directives); + + if (isMemberNotImplemented || isMemberOwnerNotImplemented) { var diagnostic = Diagnostic.Create( SupportedDiagnostics.First(), @@ -116,8 +183,6 @@ private static bool IsUnoSymbol(SymbolInfo member) } private static bool IsBindableMetadata(SyntaxNodeAnalysisContext contextAnalysis) - { - return Path.GetFileName(contextAnalysis.Node?.GetLocation()?.SourceTree?.FilePath) == "BindableMetadata.g.cs"; - } + => Path.GetFileName(contextAnalysis.Node?.GetLocation()?.SourceTree?.FilePath) == "BindableMetadata.g.cs"; } } diff --git a/src/Uno.Foundation/NotImplementedAttribute.cs b/src/Uno.Foundation/NotImplementedAttribute.cs index 288adf9c9c06..ee06276c5386 100644 --- a/src/Uno.Foundation/NotImplementedAttribute.cs +++ b/src/Uno.Foundation/NotImplementedAttribute.cs @@ -5,16 +5,29 @@ namespace Uno { + /// + /// Marks a member or symbol as not implemented by Uno. + /// [System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)] public sealed class NotImplementedAttribute : Attribute { + /// + /// Creates an instance + /// public NotImplementedAttribute() { } + /// + /// Creates an instance with C# constants for which the symbol is not implemented. + /// + /// The list of not-implemented platforms public NotImplementedAttribute(params string[] platforms) { Platforms = platforms; } + /// + /// The list of platforms that are not implemented. When empty, all platforms are not implemented. + /// public string[]? Platforms { get; } } }