diff --git a/.gitignore b/.gitignore index 022de0dfcb..e2764bd054 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,9 @@ artifacts/ docfx/artifacts/ local_packages/ *.cast + +# Graphic memory & plan markdown files +GRAPHIC_MEMORY.md +plans/ +*.skill.md + diff --git a/Directory.Packages.props b/Directory.Packages.props index 9388129c61..4880e9c406 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,9 @@ - + + + @@ -13,7 +15,8 @@ - + + diff --git a/Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj b/Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj new file mode 100644 index 0000000000..d9191013ab --- /dev/null +++ b/Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj @@ -0,0 +1,24 @@ + + + + + netstandard2.0 + Latest + enable + Terminal.Gui.Analyzers + true + true + $(NoWarn);RS2008;RS1032;RS1038 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Terminal.Gui.Analyzers/V1ApiCodeFixProvider.cs b/Terminal.Gui.Analyzers/V1ApiCodeFixProvider.cs new file mode 100644 index 0000000000..a6da94f578 --- /dev/null +++ b/Terminal.Gui.Analyzers/V1ApiCodeFixProvider.cs @@ -0,0 +1,108 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Terminal.Gui.Analyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(V1ApiCodeFixProvider)), Shared] + public class V1ApiCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(new[] { + V1ApiDiagnosticAnalyzer.RuleClicked.Id, + V1ApiDiagnosticAnalyzer.RuleToplevel.Id, + V1ApiDiagnosticAnalyzer.RuleBounds.Id, + V1ApiDiagnosticAnalyzer.RuleTabView.Id, + V1ApiDiagnosticAnalyzer.RuleRadioGroup.Id, + V1ApiDiagnosticAnalyzer.RuleAcceptingIgnoresArgs.Id }); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) + { + return; + } + + foreach (var diagnostic in context.Diagnostics) + { + var diagnosticSpan = diagnostic.Location.SourceSpan; + var token = root.FindToken(diagnosticSpan.Start); + + if (diagnostic.Id == V1ApiDiagnosticAnalyzer.RuleClicked.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Accepted'", + createChangedDocument: c => ReplaceTokenAsync(context.Document, root, token, "Accepted", c), + equivalenceKey: "UseAccepted"), + diagnostic); + } + else if (diagnostic.Id == V1ApiDiagnosticAnalyzer.RuleBounds.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Viewport'", + createChangedDocument: c => ReplaceTokenAsync(context.Document, root, token, "Viewport", c), + equivalenceKey: "UseViewport"), + diagnostic); + } + else if (diagnostic.Id == V1ApiDiagnosticAnalyzer.RuleToplevel.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Window'", + createChangedDocument: c => ReplaceTokenAsync(context.Document, root, token, "Window", c), + equivalenceKey: "UseWindow"), + diagnostic); + } + else if (diagnostic.Id == V1ApiDiagnosticAnalyzer.RuleTabView.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Tabs'", + createChangedDocument: c => ReplaceTokenAsync(context.Document, root, token, "Tabs", c), + equivalenceKey: "UseTabs"), + diagnostic); + } + else if (diagnostic.Id == V1ApiDiagnosticAnalyzer.RuleRadioGroup.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'OptionSelector'", + createChangedDocument: c => ReplaceTokenAsync(context.Document, root, token, "OptionSelector", c), + equivalenceKey: "UseOptionSelector"), + diagnostic); + } + else if (diagnostic.Id == V1ApiDiagnosticAnalyzer.RuleAcceptingIgnoresArgs.Id) + { + var node = root.FindNode(diagnosticSpan); + if (node is AssignmentExpressionSyntax assignment && assignment.Left is MemberAccessExpressionSyntax memberAccess) + { + var eventToken = memberAccess.Name.Identifier; + context.RegisterCodeFix( + CodeAction.Create( + title: "Switch event to 'Accepted'", + createChangedDocument: c => ReplaceTokenAsync(context.Document, root, eventToken, "Accepted", c), + equivalenceKey: "SwitchToAccepted"), + diagnostic); + } + } + } + } + + private static Task ReplaceTokenAsync(Document document, SyntaxNode root, SyntaxToken oldToken, string newName, CancellationToken cancellationToken) + { + var newToken = SyntaxFactory.Identifier(oldToken.LeadingTrivia, newName, oldToken.TrailingTrivia); + var newRoot = root.ReplaceToken(oldToken, newToken); + return Task.FromResult(document.WithSyntaxRoot(newRoot)); + } + } +} diff --git a/Terminal.Gui.Analyzers/V1ApiDiagnosticAnalyzer.cs b/Terminal.Gui.Analyzers/V1ApiDiagnosticAnalyzer.cs new file mode 100644 index 0000000000..c5563b4d05 --- /dev/null +++ b/Terminal.Gui.Analyzers/V1ApiDiagnosticAnalyzer.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Terminal.Gui.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class V1ApiDiagnosticAnalyzer : DiagnosticAnalyzer + { + public const string Category = "TerminalGuiV2"; + + public static readonly DiagnosticDescriptor RuleClicked = new DiagnosticDescriptor( + "TGUI0001", + "Use Accepted or Accepting instead of Clicked", + "'{0}' is a removed v1 member. In Terminal.Gui v2, use 'Accepted' (post-event) or 'Accepting' (cancellable event)", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleApplicationStatic = new DiagnosticDescriptor( + "TGUI0002", + "Use instance Application instead of static methods", + "Static Application.{0} is removed in Terminal.Gui v2. Use 'IApplication app = Application.Create().Init()', 'app.Run()', and 'app.Dispose()'", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleToplevel = new DiagnosticDescriptor( + "TGUI0003", + "Use Runnable or Window instead of Toplevel", + "'Toplevel' has been removed in Terminal.Gui v2. Inherit from 'Runnable' or use 'Window'", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleBounds = new DiagnosticDescriptor( + "TGUI0004", + "Use Viewport instead of Bounds", + "'View.Bounds' was removed in Terminal.Gui v2. Use 'View.Viewport'", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RulePositionalConstructor = new DiagnosticDescriptor( + "TGUI0005", + "Use object initializers for Views", + "Terminal.Gui v2 views use parameterless constructors and object initializers: 'new {0} {{ Text = \"...\" }}'", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleTabView = new DiagnosticDescriptor( + "TGUI0006", + "Use Tabs instead of TabView", + "'TabView' was replaced by 'Tabs' in Terminal.Gui v2. Add SubViews directly and set 'View.Title' for tab labels", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleTileView = new DiagnosticDescriptor( + "TGUI0007", + "TileView is removed in Terminal.Gui v2", + "'TileView' was removed in Terminal.Gui v2. Use Pos/Dim panes and ViewArrangement", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleRadioGroup = new DiagnosticDescriptor( + "TGUI0008", + "Use OptionSelector instead of RadioGroup", + "'RadioGroup' was replaced by 'OptionSelector' in Terminal.Gui v2", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleColorScheme = new DiagnosticDescriptor( + "TGUI0009", + "Use Scheme instead of ColorScheme", + "Terminal.Gui v2 replaces 'ColorScheme' with 'Scheme' and 'SchemeManager'", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleKeyBitmask = new DiagnosticDescriptor( + "TGUI0010", + "Use Key fluent methods instead of KeyCode bitmasks", + "Terminal.Gui v2 key bitmasks are replaced by fluent methods like 'Key.X.WithCtrl' or 'Key.X.WithAlt'", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleAcceptingIgnoresArgs = new DiagnosticDescriptor( + "TGUI0011", + "Use Accepted when event args are unused", + "The 'Accepting' event is a pre-event for cancellation. If event arguments (e.g. e.Cancel) are not used, subscribe to 'Accepted' instead", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleMenuItem6ArgCtor = new DiagnosticDescriptor( + "TGUI0012", + "Use v2 MenuItem constructor", + "Terminal.Gui v2 MenuItem constructor takes (title, help, action). Use object initializers for Shortcut/Data", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RuleLayoutStyle = new DiagnosticDescriptor( + "TGUI0013", + "LayoutStyle is removed in v2", + "'LayoutStyle' was removed in Terminal.Gui v2. All layout is declarative via Pos and Dim", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RulePosAt = new DiagnosticDescriptor( + "TGUI0014", + "Use direct integer assignment instead of Pos.At / Pos.Left", + "'Pos.At(n)' and 'Pos.Left(v)' are removed in Terminal.Gui v2. Assign integers directly e.g. X = 5", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(new[] { + RuleClicked, + RuleApplicationStatic, + RuleToplevel, + RuleBounds, + RulePositionalConstructor, + RuleTabView, + RuleTileView, + RuleRadioGroup, + RuleColorScheme, + RuleKeyBitmask, + RuleAcceptingIgnoresArgs, + RuleMenuItem6ArgCtor, + RuleLayoutStyle, + RulePosAt }); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); + context.RegisterSyntaxNodeAction(AnalyzeBitwiseOr, SyntaxKind.BitwiseOrExpression); + context.RegisterSyntaxNodeAction(AnalyzeEventSubscription, SyntaxKind.AddAssignmentExpression); + } + + private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + var memberAccess = (MemberAccessExpressionSyntax)context.Node; + var name = memberAccess.Name.Identifier.Text; + + if (name == "Clicked") + { + context.ReportDiagnostic(Diagnostic.Create(RuleClicked, memberAccess.Name.GetLocation(), memberAccess.ToString())); + } + else if (name == "Bounds") + { + var exprType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type; + if (exprType != null && IsOrInheritsFromView(exprType)) + { + context.ReportDiagnostic(Diagnostic.Create(RuleBounds, memberAccess.Name.GetLocation())); + } + } + else if (name == "ColorScheme" || name == "ColorSchemes") + { + context.ReportDiagnostic(Diagnostic.Create(RuleColorScheme, memberAccess.Name.GetLocation())); + } + else if (name == "Top" && memberAccess.Expression is IdentifierNameSyntax id && id.Identifier.Text == "Application") + { + context.ReportDiagnostic(Diagnostic.Create(RuleApplicationStatic, memberAccess.GetLocation(), "Top")); + } + else if (name == "Computed" && memberAccess.Expression is IdentifierNameSyntax ls && ls.Identifier.Text == "LayoutStyle") + { + context.ReportDiagnostic(Diagnostic.Create(RuleLayoutStyle, memberAccess.GetLocation())); + } + } + + private static bool IsOrInheritsFromView(ITypeSymbol typeSymbol) + { + var current = typeSymbol; + while (current != null) + { + if (current.Name == "View" || current.Name == "Toplevel" || current.Name == "Window" || current.Name == "Button") + { + return true; + } + current = current.BaseType; + } + return false; + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + if (memberAccess.Expression is IdentifierNameSyntax id && id.Identifier.Text == "Application") + { + var methodName = memberAccess.Name.Identifier.Text; + if (methodName == "Init" || methodName == "Run" || methodName == "Shutdown") + { + context.ReportDiagnostic(Diagnostic.Create(RuleApplicationStatic, memberAccess.GetLocation(), methodName)); + } + } + else if (memberAccess.Expression is IdentifierNameSyntax pos && pos.Identifier.Text == "Pos") + { + var posMethod = memberAccess.Name.Identifier.Text; + if (posMethod == "At" || posMethod == "Left") + { + context.ReportDiagnostic(Diagnostic.Create(RulePosAt, invocation.GetLocation())); + } + } + } + } + + private static void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) + { + var identifier = (IdentifierNameSyntax)context.Node; + var text = identifier.Identifier.Text; + + if (identifier.Parent is TypeDeclarationSyntax || + identifier.Parent is NamespaceDeclarationSyntax || + identifier.Parent is ParameterSyntax || + identifier.Parent is VariableDeclaratorSyntax) + { + return; + } + + if (text == "Toplevel") + { + context.ReportDiagnostic(Diagnostic.Create(RuleToplevel, identifier.GetLocation())); + } + else if (text == "TabView" || text == "Tab") + { + if (identifier.Parent is ObjectCreationExpressionSyntax || + identifier.Parent is VariableDeclarationSyntax || + identifier.Parent is GenericNameSyntax) + { + context.ReportDiagnostic(Diagnostic.Create(RuleTabView, identifier.GetLocation())); + } + } + else if (text == "TileView") + { + context.ReportDiagnostic(Diagnostic.Create(RuleTileView, identifier.GetLocation())); + } + else if (text == "RadioGroup") + { + context.ReportDiagnostic(Diagnostic.Create(RuleRadioGroup, identifier.GetLocation())); + } + else if (text == "LayoutStyle") + { + context.ReportDiagnostic(Diagnostic.Create(RuleLayoutStyle, identifier.GetLocation())); + } + } + + private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) + { + var objectCreation = (ObjectCreationExpressionSyntax)context.Node; + if (objectCreation.ArgumentList == null || objectCreation.ArgumentList.Arguments.Count == 0) + { + return; + } + + string? typeName = objectCreation.Type switch + { + IdentifierNameSyntax id => id.Identifier.Text, + QualifiedNameSyntax qn => qn.Right.Identifier.Text, + _ => null + }; + + if (typeName == "MenuItem" && objectCreation.ArgumentList.Arguments.Count > 3) + { + context.ReportDiagnostic(Diagnostic.Create(RuleMenuItem6ArgCtor, objectCreation.GetLocation())); + } + else if (typeName == "Button" || typeName == "Label" || typeName == "Window" || typeName == "TextField") + { + context.ReportDiagnostic(Diagnostic.Create(RulePositionalConstructor, objectCreation.GetLocation(), typeName)); + } + } + + private static void AnalyzeBitwiseOr(SyntaxNodeAnalysisContext context) + { + var binaryExpr = (BinaryExpressionSyntax)context.Node; + string exprText = binaryExpr.ToString(); + + if (exprText.Contains("CtrlMask") || exprText.Contains("AltMask") || exprText.Contains("ShiftMask")) + { + context.ReportDiagnostic(Diagnostic.Create(RuleKeyBitmask, binaryExpr.GetLocation())); + } + } + + private static void AnalyzeEventSubscription(SyntaxNodeAnalysisContext context) + { + var assignment = (AssignmentExpressionSyntax)context.Node; + if (assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "Accepting") + { + if (assignment.Right is SimpleLambdaExpressionSyntax simpleLambda) + { + if (simpleLambda.Parameter.Identifier.Text == "_") + { + context.ReportDiagnostic(Diagnostic.Create(RuleAcceptingIgnoresArgs, assignment.GetLocation())); + } + } + else if (assignment.Right is ParenthesizedLambdaExpressionSyntax lambda) + { + var paramsList = lambda.ParameterList.Parameters; + if (paramsList.Count >= 2) + { + var arg2 = paramsList[1].Identifier.Text; + if (arg2 == "_" || !LambdaBodyUsesIdentifier(lambda.Body, arg2)) + { + context.ReportDiagnostic(Diagnostic.Create(RuleAcceptingIgnoresArgs, assignment.GetLocation())); + } + } + } + } + } + + private static bool LambdaBodyUsesIdentifier(SyntaxNode body, string name) + { + if (body == null || string.IsNullOrEmpty(name) || name == "_") + { + return false; + } + + return body.DescendantNodes() + .OfType() + .Any(id => id.Identifier.Text == name); + } + } +} diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index a22f1a93f6..96c9c2241e 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -70,8 +70,14 @@ + + + + + + diff --git a/Terminal.slnx b/Terminal.slnx index 6a1b34315d..f19e783dd9 100644 --- a/Terminal.slnx +++ b/Terminal.slnx @@ -101,9 +101,12 @@ + + + diff --git a/Tests/Terminal.Gui.Analyzers.Tests/Terminal.Gui.Analyzers.Tests.csproj b/Tests/Terminal.Gui.Analyzers.Tests/Terminal.Gui.Analyzers.Tests.csproj new file mode 100644 index 0000000000..ee2b8d8ca3 --- /dev/null +++ b/Tests/Terminal.Gui.Analyzers.Tests/Terminal.Gui.Analyzers.Tests.csproj @@ -0,0 +1,28 @@ + + + Exe + true + true + enable + false + enable + net10.0 + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Terminal.Gui.Analyzers.Tests/V1ApiDiagnosticAnalyzerTests.cs b/Tests/Terminal.Gui.Analyzers.Tests/V1ApiDiagnosticAnalyzerTests.cs new file mode 100644 index 0000000000..07397c5f8a --- /dev/null +++ b/Tests/Terminal.Gui.Analyzers.Tests/V1ApiDiagnosticAnalyzerTests.cs @@ -0,0 +1,185 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Terminal.Gui.Analyzers; +using Xunit; + +namespace Terminal.Gui.Analyzers.Tests +{ + public class V1ApiDiagnosticAnalyzerTests + { + private static async Task> GetDiagnosticsAsync(string source) + { + var tree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create("TestAssembly", + new[] { tree }, + new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var analyzer = new V1ApiDiagnosticAnalyzer(); + var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); + return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); + } + + [Fact] + public void TestAnalyzerSupportedDiagnostics() + { + var analyzer = new V1ApiDiagnosticAnalyzer(); + Assert.Equal(14, analyzer.SupportedDiagnostics.Length); + } + + [Fact] + public async Task TestClickedTriggersDiagnostic() + { + var source = @" +class Test { + void Demo(dynamic btn) { + btn.Clicked += null; + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleClicked.Id); + } + + [Fact] + public async Task TestStaticApplicationTriggersDiagnostic() + { + var source = @" +class Application { public static void Init() {} } +class Test { + void Demo() { + Application.Init(); + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleApplicationStatic.Id); + } + + [Fact] + public async Task TestToplevelTriggersDiagnostic() + { + var source = @" +class Toplevel {} +class Test { + Toplevel top; +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleToplevel.Id); + } + + [Fact] + public async Task TestPositionalConstructorTriggersDiagnostic() + { + var source = @" +class Button { public Button(string text) {} } +class Test { + void Demo() { + var b = new Button(""OK""); + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RulePositionalConstructor.Id); + } + + [Fact] + public async Task TestTabViewTriggersDiagnostic() + { + var source = @" +class TabView {} +class Test { + void Demo() { + var tv = new TabView(); + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleTabView.Id); + } + + [Fact] + public async Task TestRadioGroupTriggersDiagnostic() + { + var source = @" +class RadioGroup {} +class Test { + void Demo() { + var rg = new RadioGroup(); + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleRadioGroup.Id); + } + + [Fact] + public async Task TestKeyBitmaskTriggersDiagnostic() + { + var source = @" +enum KeyCode { A, CtrlMask } +class Test { + void Demo() { + var k = KeyCode.A | KeyCode.CtrlMask; + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleKeyBitmask.Id); + } + + [Fact] + public async Task TestAcceptingIgnoredArgsTriggersDiagnostic() + { + var source = @" +class View { public event System.EventHandler Accepting; } +class Test { + void Demo(View view) { + view.Accepting += (_, _) => { }; + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleAcceptingIgnoresArgs.Id); + } + + [Fact] + public async Task TestMenuItem6ArgCtorTriggersDiagnostic() + { + var source = @" +class MenuItem { public MenuItem(string a, string b, System.Action c, System.Func d, object e, object f) {} } +class Test { + void Demo() { + var mi = new MenuItem(""a"", ""b"", null, null, null, null); + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleMenuItem6ArgCtor.Id); + } + + [Fact] + public async Task TestLayoutStyleTriggersDiagnostic() + { + var source = @" +class LayoutStyle { public static object Computed; } +class Test { + void Demo() { + var style = LayoutStyle.Computed; + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RuleLayoutStyle.Id); + } + + [Fact] + public async Task TestPosAtTriggersDiagnostic() + { + var source = @" +class Pos { public static object At(int n) => null; } +class Test { + void Demo() { + var p = Pos.At(5); + } +}"; + var diagnostics = await GetDiagnosticsAsync(source); + Assert.Contains(diagnostics, d => d.Id == V1ApiDiagnosticAnalyzer.RulePosAt.Id); + } + } +} diff --git a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj index 096f6afe44..97b9a9b8b3 100644 --- a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj +++ b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj @@ -1,4 +1,4 @@ - + Exe true @@ -27,8 +27,10 @@ + + @@ -47,7 +49,9 @@ + +