Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
61 changes: 61 additions & 0 deletions Terminal.Gui.Analyzers.Tests/HandledEventArgsAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 165 additions & 0 deletions Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions Terminal.Gui.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Release 1.0.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
5 changes: 5 additions & 0 deletions Terminal.Gui.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
TGUI001 | Reliability | Warning | HandledEventArgsAnalyzer, [Documentation](./TGUI001.md)
67 changes: 67 additions & 0 deletions Terminal.Gui.Analyzers/DiagnosticCategory.cs
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading