mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
This commit is contained in:
@@ -5,11 +5,16 @@
|
||||
<ItemGroup>
|
||||
<!-- Enable Nuget Source Link for github -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Features" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
|
||||
<PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="[2024.3.0,)" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.11,4.12)" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.11,4.12)" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="[9.0.2,10)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Terminal.Gui.Input;
|
||||
using Terminal.Gui.Views;
|
||||
|
||||
namespace Terminal.Gui.Analyzers.Tests;
|
||||
|
||||
public class HandledEventArgsAnalyzerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("e")]
|
||||
[InlineData ("args")]
|
||||
public async Task Should_ReportDiagnostic_When_EHandledNotSet_Lambda (string paramName)
|
||||
{
|
||||
var originalCode = $$"""
|
||||
using Terminal.Gui.Views;
|
||||
|
||||
class TestClass
|
||||
{
|
||||
void Setup()
|
||||
{
|
||||
var b = new Button();
|
||||
b.Accepting += (s, {{paramName}}) =>
|
||||
{
|
||||
// Forgot {{paramName}}.Handled = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
""";
|
||||
await new ProjectBuilder ()
|
||||
.WithSourceCode (originalCode)
|
||||
.WithAnalyzer (new HandledEventArgsAnalyzer ())
|
||||
.ValidateAsync ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData ("e")]
|
||||
[InlineData ("args")]
|
||||
public async Task Should_ReportDiagnostic_When_EHandledNotSet_Method (string paramName)
|
||||
{
|
||||
var originalCode = $$"""
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.Input;
|
||||
|
||||
class TestClass
|
||||
{
|
||||
void Setup()
|
||||
{
|
||||
var b = new Button();
|
||||
b.Accepting += BOnAccepting;
|
||||
}
|
||||
private void BOnAccepting (object? sender, CommandEventArgs {{paramName}})
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
""";
|
||||
await new ProjectBuilder ()
|
||||
.WithSourceCode (originalCode)
|
||||
.WithAnalyzer (new HandledEventArgsAnalyzer ())
|
||||
.ValidateAsync ();
|
||||
}
|
||||
}
|
||||
165
Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs
Normal file
165
Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Terminal.Gui.ViewBase;
|
||||
using Terminal.Gui.Views;
|
||||
using Document = Microsoft.CodeAnalysis.Document;
|
||||
using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
public sealed class ProjectBuilder
|
||||
{
|
||||
private string _sourceCode;
|
||||
private string _expectedFixedCode;
|
||||
private DiagnosticAnalyzer _analyzer;
|
||||
private CodeFixProvider _codeFix;
|
||||
|
||||
public ProjectBuilder WithSourceCode (string source)
|
||||
{
|
||||
_sourceCode = source;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProjectBuilder ShouldFixCodeWith (string expected)
|
||||
{
|
||||
_expectedFixedCode = expected;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProjectBuilder WithAnalyzer (DiagnosticAnalyzer analyzer)
|
||||
{
|
||||
_analyzer = analyzer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProjectBuilder WithCodeFix (CodeFixProvider codeFix)
|
||||
{
|
||||
_codeFix = codeFix;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync ()
|
||||
{
|
||||
if (_sourceCode == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Source code not set.");
|
||||
}
|
||||
|
||||
if (_analyzer == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Analyzer not set.");
|
||||
}
|
||||
|
||||
// Parse original document
|
||||
var document = CreateDocument (_sourceCode);
|
||||
var compilation = await document.Project.GetCompilationAsync ();
|
||||
|
||||
var diagnostics = compilation.GetDiagnostics ();
|
||||
var errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error);
|
||||
|
||||
if (errors.Any ())
|
||||
{
|
||||
var errorMessages = string.Join (Environment.NewLine, errors.Select (e => e.ToString ()));
|
||||
throw new Exception ("Compilation failed with errors:" + Environment.NewLine + errorMessages);
|
||||
}
|
||||
|
||||
// Run analyzer
|
||||
var analyzerDiagnostics = await GetAnalyzerDiagnosticsAsync (compilation, _analyzer);
|
||||
|
||||
Assert.NotEmpty (analyzerDiagnostics);
|
||||
|
||||
if (_expectedFixedCode != null)
|
||||
{
|
||||
if (_codeFix == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Expected code fix but none was set.");
|
||||
}
|
||||
|
||||
var fixedDocument = await ApplyCodeFixAsync (document, analyzerDiagnostics.First (), _codeFix);
|
||||
|
||||
var formattedDocument = await Formatter.FormatAsync (fixedDocument);
|
||||
var fixedSource = (await formattedDocument.GetTextAsync ()).ToString ();
|
||||
|
||||
Assert.Equal (_expectedFixedCode, fixedSource);
|
||||
}
|
||||
}
|
||||
|
||||
private static Document CreateDocument (string source)
|
||||
{
|
||||
var dd = typeof (Enumerable).GetTypeInfo ().Assembly.Location;
|
||||
var coreDir = Directory.GetParent (dd) ?? throw new Exception ($"Could not find parent directory of dotnet sdk. Sdk directory was {dd}");
|
||||
|
||||
var workspace = new AdhocWorkspace ();
|
||||
var projectId = ProjectId.CreateNewId ();
|
||||
var documentId = DocumentId.CreateNewId (projectId);
|
||||
|
||||
var references = new List<MetadataReference> ()
|
||||
{
|
||||
MetadataReference.CreateFromFile(typeof(Button).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(View).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(System.IO.FileSystemInfo).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(MarshalByValueComponent).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(ObservableCollection<string>).Assembly.Location),
|
||||
|
||||
// New assemblies required by Terminal.Gui version 2
|
||||
MetadataReference.CreateFromFile(typeof(Size).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(CanBeNullAttribute).Assembly.Location),
|
||||
|
||||
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "mscorlib.dll")),
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Runtime.dll")),
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Collections.dll")),
|
||||
MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Data.Common.dll")),
|
||||
// Add more as necessary
|
||||
};
|
||||
|
||||
|
||||
var projectInfo = ProjectInfo.Create (
|
||||
projectId,
|
||||
VersionStamp.Create (),
|
||||
"TestProject",
|
||||
"TestAssembly",
|
||||
LanguageNames.CSharp,
|
||||
compilationOptions: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary),
|
||||
metadataReferences: references);
|
||||
|
||||
var solution = workspace.CurrentSolution
|
||||
.AddProject (projectInfo)
|
||||
.AddDocument (documentId, "Test.cs", SourceText.From (source));
|
||||
|
||||
return solution.GetDocument (documentId)!;
|
||||
}
|
||||
|
||||
private static async Task<ImmutableArray<Diagnostic>> GetAnalyzerDiagnosticsAsync (Compilation compilation, DiagnosticAnalyzer analyzer)
|
||||
{
|
||||
var compilationWithAnalyzers = compilation.WithAnalyzers (ImmutableArray.Create (analyzer));
|
||||
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync ();
|
||||
}
|
||||
|
||||
private static async Task<Document> ApplyCodeFixAsync (Document document, Diagnostic diagnostic, CodeFixProvider codeFix)
|
||||
{
|
||||
CodeAction _codeAction = null;
|
||||
var context = new CodeFixContext ((TextDocument)document, diagnostic, (action, _) => _codeAction = action, CancellationToken.None);
|
||||
|
||||
await codeFix.RegisterCodeFixesAsync (context);
|
||||
|
||||
if (_codeAction == null)
|
||||
{
|
||||
throw new InvalidOperationException ("Code fix did not register a fix.");
|
||||
}
|
||||
|
||||
var operations = await _codeAction.GetOperationsAsync (CancellationToken.None);
|
||||
var solution = operations.OfType<ApplyChangesOperation> ().First ().ChangedSolution;
|
||||
return solution.GetDocument (document.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<DefineConstants>$(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL</DefineConstants>
|
||||
<DebugType>portable</DebugType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoLogo>true</NoLogo>
|
||||
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Features" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Terminal.Gui.Analyzers\Terminal.Gui.Analyzers.csproj" />
|
||||
<ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
6
Terminal.Gui.Analyzers/AnalyzerReleases.Shipped.md
Normal file
6
Terminal.Gui.Analyzers/AnalyzerReleases.Shipped.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## Release 1.0.0
|
||||
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
--------|----------|----------|--------------------
|
||||
5
Terminal.Gui.Analyzers/AnalyzerReleases.Unshipped.md
Normal file
5
Terminal.Gui.Analyzers/AnalyzerReleases.Unshipped.md
Normal file
@@ -0,0 +1,5 @@
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
--------|----------|----------|--------------------
|
||||
TGUI001 | Reliability | Warning | HandledEventArgsAnalyzer, [Documentation](./TGUI001.md)
|
||||
67
Terminal.Gui.Analyzers/DiagnosticCategory.cs
Normal file
67
Terminal.Gui.Analyzers/DiagnosticCategory.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
namespace Terminal.Gui.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Categories commonly used for diagnostic analyzers, inspired by FxCop and .NET analyzers conventions.
|
||||
/// </summary>
|
||||
internal enum DiagnosticCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// Issues related to naming conventions and identifiers.
|
||||
/// </summary>
|
||||
Naming,
|
||||
|
||||
/// <summary>
|
||||
/// API design, class structure, inheritance, etc.
|
||||
/// </summary>
|
||||
Design,
|
||||
|
||||
/// <summary>
|
||||
/// How code uses APIs or language features incorrectly or suboptimally.
|
||||
/// </summary>
|
||||
Usage,
|
||||
|
||||
/// <summary>
|
||||
/// Patterns that cause poor runtime performance.
|
||||
/// </summary>
|
||||
Performance,
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerabilities or insecure coding patterns.
|
||||
/// </summary>
|
||||
Security,
|
||||
|
||||
/// <summary>
|
||||
/// Code patterns that can cause bugs, crashes, or unpredictable behavior.
|
||||
/// </summary>
|
||||
Reliability,
|
||||
|
||||
/// <summary>
|
||||
/// Code readability, complexity, or future-proofing concerns.
|
||||
/// </summary>
|
||||
Maintainability,
|
||||
|
||||
/// <summary>
|
||||
/// Code patterns that may not work on all platforms or frameworks.
|
||||
/// </summary>
|
||||
Portability,
|
||||
|
||||
/// <summary>
|
||||
/// Issues with culture, localization, or globalization support.
|
||||
/// </summary>
|
||||
Globalization,
|
||||
|
||||
/// <summary>
|
||||
/// Problems when working with COM, P/Invoke, or other interop scenarios.
|
||||
/// </summary>
|
||||
Interoperability,
|
||||
|
||||
/// <summary>
|
||||
/// Issues with missing or incorrect XML doc comments.
|
||||
/// </summary>
|
||||
Documentation,
|
||||
|
||||
/// <summary>
|
||||
/// Purely stylistic issues not affecting semantics (e.g., whitespace, order).
|
||||
/// </summary>
|
||||
Style
|
||||
}
|
||||
269
Terminal.Gui.Analyzers/HandledEventArgsAnalyzer.cs
Normal file
269
Terminal.Gui.Analyzers/HandledEventArgsAnalyzer.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using System.Collections.Immutable;
|
||||
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 HandledEventArgsAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public const string DiagnosticId = "TGUI001";
|
||||
private static readonly LocalizableString Title = "Accepting event handler should set Handled = true";
|
||||
private static readonly LocalizableString MessageFormat = "Accepting event handler does not set Handled = true";
|
||||
private static readonly LocalizableString Description = "Handlers for Accepting should mark the CommandEventArgs as handled by setting Handled = true otherwise subsequent Accepting event handlers may also fire (e.g. default buttons).";
|
||||
private static readonly string Url = "https://github.com/tznind/gui.cs/blob/analyzer-no-handled/Terminal.Gui.Analyzers/TGUI001.md";
|
||||
private const string Category = nameof(DiagnosticCategory.Reliability);
|
||||
|
||||
private static readonly DiagnosticDescriptor _rule = new (
|
||||
DiagnosticId,
|
||||
Title,
|
||||
MessageFormat,
|
||||
Category,
|
||||
DiagnosticSeverity.Warning,
|
||||
true,
|
||||
Description,
|
||||
helpLinkUri: Url);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];
|
||||
|
||||
public override void Initialize (AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution ();
|
||||
|
||||
// Only analyze non-generated code
|
||||
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None);
|
||||
|
||||
// Register for b.Accepting += (s,e)=>{...};
|
||||
context.RegisterSyntaxNodeAction (
|
||||
AnalyzeLambdaOrAnonymous,
|
||||
SyntaxKind.ParenthesizedLambdaExpression,
|
||||
SyntaxKind.SimpleLambdaExpression,
|
||||
SyntaxKind.AnonymousMethodExpression);
|
||||
|
||||
// Register for b.Accepting += MyMethod;
|
||||
context.RegisterSyntaxNodeAction (
|
||||
AnalyzeEventSubscriptionWithMethodGroup,
|
||||
SyntaxKind.AddAssignmentExpression);
|
||||
}
|
||||
|
||||
private static void AnalyzeLambdaOrAnonymous (SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var lambda = (AnonymousFunctionExpressionSyntax)context.Node;
|
||||
|
||||
// Check if this lambda is assigned to the Accepting event
|
||||
if (!IsAssignedToAcceptingEvent (lambda.Parent, context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for any parameter of type CommandEventArgs (regardless of name)
|
||||
IParameterSymbol? eParam = GetCommandEventArgsParameter (lambda, context.SemanticModel);
|
||||
|
||||
if (eParam == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Analyze lambda body for e.Handled = true assignment
|
||||
if (lambda.Body is BlockSyntax block)
|
||||
{
|
||||
bool setsHandled = block.Statements
|
||||
.SelectMany (s => s.DescendantNodes ().OfType<AssignmentExpressionSyntax> ())
|
||||
.Any (a => IsHandledAssignment (a, eParam, context));
|
||||
|
||||
if (!setsHandled)
|
||||
{
|
||||
var diag = Diagnostic.Create (_rule, lambda.GetLocation ());
|
||||
context.ReportDiagnostic (diag);
|
||||
}
|
||||
}
|
||||
else if (lambda.Body is ExpressionSyntax)
|
||||
{
|
||||
// Expression-bodied lambdas unlikely for event handlers — skip
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first parameter of type CommandEventArgs in any parameter list (method or lambda).
|
||||
/// </summary>
|
||||
/// <param name="paramOwner"></param>
|
||||
/// <param name="semanticModel"></param>
|
||||
/// <returns></returns>
|
||||
private static IParameterSymbol? GetCommandEventArgsParameter (SyntaxNode paramOwner, SemanticModel semanticModel)
|
||||
{
|
||||
SeparatedSyntaxList<ParameterSyntax>? parameters = paramOwner switch
|
||||
{
|
||||
AnonymousFunctionExpressionSyntax lambda => GetParameters (lambda),
|
||||
MethodDeclarationSyntax method => method.ParameterList.Parameters,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (parameters == null || parameters.Value.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (ParameterSyntax param in parameters.Value)
|
||||
{
|
||||
IParameterSymbol? symbol = semanticModel.GetDeclaredSymbol (param);
|
||||
|
||||
if (symbol != null && IsCommandEventArgsType (symbol.Type))
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsAssignedToAcceptingEvent (SyntaxNode? node, SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (node is AssignmentExpressionSyntax assignment && IsAcceptingEvent (assignment.Left, context))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node?.Parent is AssignmentExpressionSyntax parentAssignment && IsAcceptingEvent (parentAssignment.Left, context))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsCommandEventArgsType (ITypeSymbol? type) { return type != null && type.Name == "CommandEventArgs"; }
|
||||
|
||||
private static void AnalyzeEventSubscriptionWithMethodGroup (SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var assignment = (AssignmentExpressionSyntax)context.Node;
|
||||
|
||||
// Check event name: b.Accepting += ...
|
||||
if (!IsAcceptingEvent (assignment.Left, context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Right side: should be method group (IdentifierNameSyntax)
|
||||
if (assignment.Right is IdentifierNameSyntax methodGroup)
|
||||
{
|
||||
// Resolve symbol of method group
|
||||
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo (methodGroup);
|
||||
|
||||
if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
|
||||
{
|
||||
// Find method declaration in syntax tree
|
||||
ImmutableArray<SyntaxReference> declRefs = methodSymbol.DeclaringSyntaxReferences;
|
||||
|
||||
foreach (SyntaxReference declRef in declRefs)
|
||||
{
|
||||
var methodDecl = declRef.GetSyntax () as MethodDeclarationSyntax;
|
||||
|
||||
if (methodDecl != null)
|
||||
{
|
||||
AnalyzeHandlerMethodBody (context, methodDecl, methodSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnalyzeHandlerMethodBody (SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDecl, IMethodSymbol methodSymbol)
|
||||
{
|
||||
// Look for any parameter of type CommandEventArgs
|
||||
IParameterSymbol? eParam = GetCommandEventArgsParameter (methodDecl, context.SemanticModel);
|
||||
|
||||
if (eParam == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Analyze method body
|
||||
if (methodDecl.Body != null)
|
||||
{
|
||||
bool setsHandled = methodDecl.Body.Statements
|
||||
.SelectMany (s => s.DescendantNodes ().OfType<AssignmentExpressionSyntax> ())
|
||||
.Any (a => IsHandledAssignment (a, eParam, context));
|
||||
|
||||
if (!setsHandled)
|
||||
{
|
||||
var diag = Diagnostic.Create (_rule, methodDecl.Identifier.GetLocation ());
|
||||
context.ReportDiagnostic (diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SeparatedSyntaxList<ParameterSyntax> GetParameters (AnonymousFunctionExpressionSyntax lambda)
|
||||
{
|
||||
switch (lambda)
|
||||
{
|
||||
case ParenthesizedLambdaExpressionSyntax p:
|
||||
return p.ParameterList.Parameters;
|
||||
case SimpleLambdaExpressionSyntax s:
|
||||
// Simple lambda has a single parameter, wrap it in a list
|
||||
return SyntaxFactory.SeparatedList (new [] { s.Parameter });
|
||||
case AnonymousMethodExpressionSyntax a:
|
||||
return a.ParameterList?.Parameters ?? default (SeparatedSyntaxList<ParameterSyntax>);
|
||||
default:
|
||||
return default (SeparatedSyntaxList<ParameterSyntax>);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAcceptingEvent (ExpressionSyntax expr, SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
// Check if expr is b.Accepting or similar
|
||||
|
||||
// Get symbol info
|
||||
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo (expr);
|
||||
ISymbol? symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Accepting event symbol should be an event named "Accepting"
|
||||
if (symbol.Kind == SymbolKind.Event && symbol.Name == "Accepting")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsHandledAssignment (AssignmentExpressionSyntax assignment, IParameterSymbol eParamSymbol, SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
// Check if left side is "e.Handled" and right side is "true"
|
||||
// Left side should be MemberAccessExpression: e.Handled
|
||||
|
||||
if (assignment.Left is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
// Check that member access expression is "e.Handled"
|
||||
ISymbol? exprSymbol = context.SemanticModel.GetSymbolInfo (memberAccess.Expression).Symbol;
|
||||
|
||||
if (exprSymbol == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SymbolEqualityComparer.Default.Equals (exprSymbol, eParamSymbol))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memberAccess.Name.Identifier.Text != "Handled")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check right side is true literal
|
||||
if (assignment.Right is LiteralExpressionSyntax literal && literal.IsKind (SyntaxKind.TrueLiteralExpression))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
34
Terminal.Gui.Analyzers/TGUI001.md
Normal file
34
Terminal.Gui.Analyzers/TGUI001.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# TGUI001: Describe what your rule checks
|
||||
|
||||
**Category:** Reliability
|
||||
**Severity:** Warning
|
||||
**Enabled by default:** Yes
|
||||
|
||||
## Cause
|
||||
|
||||
When registering an event handler for `Accepting`, you should set Handled to true, this prevents other subsequent Views from responding to the same input event.
|
||||
|
||||
## Reason for rule
|
||||
|
||||
If you do not do this then you may see unpredictable behaviour such as clicking a Button resulting in another `IsDefault` button in the View also firing.
|
||||
|
||||
See:
|
||||
|
||||
- https://github.com/gui-cs/Terminal.Gui/issues/3913
|
||||
- https://github.com/gui-cs/Terminal.Gui/issues/4170
|
||||
|
||||
## How to fix violations
|
||||
|
||||
Set Handled to `true` in your event handler
|
||||
|
||||
### Examples
|
||||
|
||||
```diff
|
||||
var b = new Button();
|
||||
b.Accepting += (s, e) =>
|
||||
{
|
||||
// Do something
|
||||
|
||||
+ e.Handled = true;
|
||||
};
|
||||
```
|
||||
18
Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj
Normal file
18
Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<!-- Analyzer only, no executable -->
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -61,7 +61,11 @@
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="Wcwidth" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Terminal.Gui.Analyzers\Terminal.Gui.Analyzers.csproj"
|
||||
ReferenceOutputAssembly="false"
|
||||
OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Enable Nuget Source Link for github -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
|
||||
@@ -155,6 +159,16 @@
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
|
||||
|
||||
<Target Name="CopyAnalyzersToPackage" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<AnalyzerFiles Include="..\Terminal.Gui.Analyzers\bin\$(Configuration)\netstandard2.0\Terminal.Gui.Analyzers.dll" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<None Include="..\Terminal.Gui.Analyzers\bin\$(Configuration)\netstandard2.0\Terminal.Gui.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--<Target Name="PreBuildCleanup" BeforeTargets="BeforeBuild" Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<Exec Command="rmdir /s /q "$(UserProfile)\.nuget\packages\terminal.gui\2.0.0"" Condition=" '$(OS)' == 'Windows_NT' " />
|
||||
<Exec Command="rm -rf ~/.nuget/packages/terminal.gui/2.0.0" Condition=" '$(OS)' != 'Windows_NT' " />
|
||||
|
||||
12
Terminal.sln
12
Terminal.sln
@@ -75,6 +75,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXun
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit.Generator", "Tests\TerminalGuiFluentTestingXunit.Generator\TerminalGuiFluentTestingXunit.Generator.csproj", "{199F27D8-A905-4DDC-82CA-1FE1A90B1788}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers", "Terminal.Gui.Analyzers\Terminal.Gui.Analyzers.csproj", "{D1D68BA7-8476-448E-8BA3-927C15933119}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers.Tests", "Terminal.Gui.Analyzers.Tests\Terminal.Gui.Analyzers.Tests.csproj", "{8C643A64-2A77-4432-987A-2E72BD9708E3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -141,6 +145,14 @@ Global
|
||||
{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D1D68BA7-8476-448E-8BA3-927C15933119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D1D68BA7-8476-448E-8BA3-927C15933119}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D1D68BA7-8476-448E-8BA3-927C15933119}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D1D68BA7-8476-448E-8BA3-927C15933119}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8C643A64-2A77-4432-987A-2E72BD9708E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C643A64-2A77-4432-987A-2E72BD9708E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C643A64-2A77-4432-987A-2E72BD9708E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C643A64-2A77-4432-987A-2E72BD9708E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.Net.Compilers.Toolset" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.Net.Compilers.Toolset" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user