mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 03:27:41 +00:00
Moved all extension methods into extension blocks (#1155)
This commit is contained in:
1 parent
acc7d09fa2
commit
c0a75f39ac
138 files changed
+5884
-4954
No files matched your search
@@ -0,0 +1,69 @@
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
using SourceCodeRules.StyleAnalyzers;
|
||||
|
||||
namespace AIStudio.Tests.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that extension methods are declared in extension blocks, not with a this parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AI Studio declares all its extension members in extension blocks, one per receiver. MWAIS0016 refuses the
|
||||
/// classic form while compiling, except for a method with a params parameter: for a member of an extension
|
||||
/// block, the compiler of the .NET 10.0.1xx SDK warns about every argument passed one by one to a params
|
||||
/// parameter (CS8620, dotnet/roslyn#80024).
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ClassicExtensionMethodAnalyzerTests
|
||||
{
|
||||
[TestCase("public static string Label(this Meeting meeting) => meeting.Name;", "Label", TestName = "A public extension method")]
|
||||
[TestCase("private static string Label(this Meeting meeting) => meeting.Name;", "Label", TestName = "A private extension method")]
|
||||
[TestCase("public static string Label(this Meeting meeting, string prefix) => prefix + meeting.Name;", "Label", TestName = "An extension method with further parameters")]
|
||||
[TestCase("public static T Echo<T>(this T value) where T : class => value;", "Echo", TestName = "A generic extension method")]
|
||||
[TestCase("public static void Bump(this ref int value) => value++;", "Bump", TestName = "An extension method with a ref receiver")]
|
||||
[TestCase("public static System.Threading.Tasks.Task<string> LabelAsync(this Meeting meeting) => System.Threading.Tasks.Task.FromResult(meeting.Name);", "LabelAsync", TestName = "An asynchronous extension method")]
|
||||
public async Task AClassicExtensionMethodIsRefused(string member, string name)
|
||||
{
|
||||
var reported = await AnalyzeAsync(member);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0016"));
|
||||
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"'{name}'"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("extension(Meeting meeting) { public string Label() => meeting.Name; }", TestName = "A member of an extension block")]
|
||||
[TestCase("extension<T>(T value) where T : class { public T Echo() => value; }", TestName = "A member of a generic extension block")]
|
||||
[TestCase("public static string Label(Meeting meeting) => meeting.Name;", TestName = "A static method without a this parameter")]
|
||||
[TestCase("public static string Label(this Meeting meeting, params string[] prefixes) => string.Concat(prefixes) + meeting.Name;", TestName = "An extension method with a params parameter")]
|
||||
public async Task AnythingElseIsLeftAlone(string member)
|
||||
{
|
||||
var reported = await AnalyzeAsync(member);
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<Diagnostic>> AnalyzeAsync(string member)
|
||||
{
|
||||
var source =
|
||||
$$"""
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Meeting
|
||||
{
|
||||
public string Name { get; set; } = "Kickoff";
|
||||
}
|
||||
|
||||
public static class MeetingExtensions
|
||||
{
|
||||
{{member}}
|
||||
}
|
||||
""";
|
||||
|
||||
return await CompilationHarness.AnalyzeAsync(source, new ClassicExtensionMethodAnalyzer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
using SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
namespace AIStudio.Tests.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the texts of the app are looked up only through a T or TB wrapper, whose fallback text the I18N
|
||||
/// collector finds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The collector reads the texts from calls of T and TB. A text handed to GetText anywhere else never reaches
|
||||
/// the language plugins, so it is never translated, and nothing at runtime shows it. MWAIS0012 refuses such a
|
||||
/// call while compiling. GetText is an extension method, so these tests ask in both of the forms C# knows for
|
||||
/// one: the classic method with a this parameter and the member of an extension block.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class DirectI18NGetTextAnalyzerTests
|
||||
{
|
||||
private const string CLASSIC_FORM =
|
||||
"""
|
||||
public static class ILangExtensions
|
||||
{
|
||||
public static string GetText(this ILang lang, string fallbackEN) => fallbackEN;
|
||||
}
|
||||
""";
|
||||
|
||||
private const string EXTENSION_BLOCK_FORM =
|
||||
"""
|
||||
public static class ILangExtensions
|
||||
{
|
||||
extension(ILang lang)
|
||||
{
|
||||
public string GetText(string fallbackEN) => fallbackEN;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[TestCase(CLASSIC_FORM, "T", TestName = "A T wrapper around a classic extension method")]
|
||||
[TestCase(CLASSIC_FORM, "TB", TestName = "A TB wrapper around a classic extension method")]
|
||||
[TestCase(EXTENSION_BLOCK_FORM, "T", TestName = "A T wrapper around the member of an extension block")]
|
||||
[TestCase(EXTENSION_BLOCK_FORM, "TB", TestName = "A TB wrapper around the member of an extension block")]
|
||||
public async Task AWrapperWhichForwardsItsTextIsAllowed(string form, string wrapper)
|
||||
{
|
||||
var reported = await AnalyzeAsync(form, $"public string {wrapper}(string fallbackEN) => this.GetText(fallbackEN);");
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase(CLASSIC_FORM, """public string Describe() => this.GetText("Hello");""", TestName = "A classic extension method called from another method")]
|
||||
[TestCase(CLASSIC_FORM, """public string T(string fallbackEN) => this.GetText("Hello");""", TestName = "A classic extension method called by a wrapper with a text of its own")]
|
||||
[TestCase(CLASSIC_FORM, """public string Describe() => ILangExtensions.GetText(this, "Hello");""", TestName = "A classic extension method called as a static method")]
|
||||
[TestCase(EXTENSION_BLOCK_FORM, """public string Describe() => this.GetText("Hello");""", TestName = "The member of an extension block called from another method")]
|
||||
[TestCase(EXTENSION_BLOCK_FORM, """public string T(string fallbackEN) => this.GetText("Hello");""", TestName = "The member of an extension block called by a wrapper with a text of its own")]
|
||||
[TestCase(EXTENSION_BLOCK_FORM, """public string Describe() => ILangExtensions.GetText(this, "Hello");""", TestName = "The member of an extension block called as a static method")]
|
||||
public async Task ADirectLookupIsRefused(string form, string member)
|
||||
{
|
||||
var reported = await AnalyzeAsync(form, member);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0012"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("this.GetText(PLUGIN, fallbackEN)", 0, TestName = "The GetText of the app in a wrapper")]
|
||||
[TestCase("""this.GetText(PLUGIN, "Hello")""", 1, TestName = "The GetText of the app in a wrapper with a text of its own")]
|
||||
public async Task TheGetTextOfTheAppIsRecognized(string lookup, int expectedReports)
|
||||
{
|
||||
//
|
||||
// Whatever form the app gives its GetText, the analyzer has to know it:
|
||||
//
|
||||
var source =
|
||||
$$"""
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Component : ILang
|
||||
{
|
||||
private static readonly ILanguagePlugin PLUGIN = NoPluginLanguage.INSTANCE;
|
||||
|
||||
public string T(string fallbackEN) => {{lookup}};
|
||||
|
||||
public string T(string fallbackEN, string? typeNamespace, string? typeName) => fallbackEN;
|
||||
}
|
||||
""";
|
||||
|
||||
var reported = await CompilationHarness.AnalyzeAsync(source, new DirectI18NGetTextAnalyzer());
|
||||
|
||||
Assert.That(reported, Has.Count.EqualTo(expectedReports));
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<Diagnostic>> AnalyzeAsync(string form, string member)
|
||||
{
|
||||
//
|
||||
// The snippet declares its own ILang and ILangExtensions where the analyzer looks for them. They take
|
||||
// the place of those of the app, which the compilation references as well.
|
||||
//
|
||||
var source =
|
||||
$$"""
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public interface ILang
|
||||
{
|
||||
}
|
||||
|
||||
{{form}}
|
||||
|
||||
public sealed class Component : ILang
|
||||
{
|
||||
{{member}}
|
||||
}
|
||||
""";
|
||||
|
||||
return await CompilationHarness.AnalyzeAsync(source, new DirectI18NGetTextAnalyzer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
using SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
namespace AIStudio.Tests.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which members MWAIS0005 wants to be reached through <c>this.</c>, and that extension methods are never
|
||||
/// asked for it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An extension member has no <c>this</c>: a classic extension method is static, and in an extension block C#
|
||||
/// refuses the keyword. There, every instance member is reached through the name of the receiver, and a sibling
|
||||
/// member without it does not even compile. Should a later version of C# reach the receiver without its name,
|
||||
/// the analyzer would demand a <c>this.</c> which nobody can write, and the tests for extension blocks fail.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ThisUsageAnalyzerTests
|
||||
{
|
||||
[TestCase("this.location", TestName = "A field through this")]
|
||||
[TestCase("this.Name", TestName = "A property through this")]
|
||||
[TestCase("this.Describe()", TestName = "A method through this")]
|
||||
[TestCase("CITY", TestName = "A static field")]
|
||||
[TestCase("ROOM", TestName = "A constant")]
|
||||
[TestCase("Shorten(this.location)", TestName = "A static method")]
|
||||
public async Task AMemberWhichNeedsNoThisIsLeftAlone(string expression)
|
||||
{
|
||||
var reported = await AnalyzeClassAsync(expression);
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase("location", TestName = "A field without this")]
|
||||
[TestCase("Name", TestName = "A property without this")]
|
||||
[TestCase("Describe()", TestName = "A method without this")]
|
||||
public async Task AnInstanceMemberWithoutThisIsRefused(string expression)
|
||||
{
|
||||
var reported = await AnalyzeClassAsync(expression);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0005"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AClassicExtensionMethodIsLeftAlone()
|
||||
{
|
||||
var source =
|
||||
"""
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Meeting
|
||||
{
|
||||
public string Name { get; set; } = "Kickoff";
|
||||
}
|
||||
|
||||
public static class MeetingExtensions
|
||||
{
|
||||
private static readonly string PREFIX = "Meeting: ";
|
||||
|
||||
public static string Label(this Meeting meeting) => PREFIX + meeting.Name;
|
||||
|
||||
public static string Title(this Meeting meeting) => Label(meeting).ToUpperInvariant();
|
||||
}
|
||||
""";
|
||||
|
||||
var reported = await CompilationHarness.AnalyzeAsync(source, new ThisUsageAnalyzer());
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase("public string Label() => meeting.Name;", TestName = "A member of the receiver")]
|
||||
[TestCase("public string Label() => meeting.Title();", TestName = "A sibling through the receiver")]
|
||||
[TestCase("public string Label() => Title(meeting);", TestName = "A sibling as a static method")]
|
||||
[TestCase("""public string Label() => PREFIX + SUFFIX + TB("Meeting");""", TestName = "The static members of the class")]
|
||||
[TestCase("public string Label() { System.Func<string> title = () => meeting.Title(); return title(); }", TestName = "A lambda")]
|
||||
[TestCase("public string Label() { string Title() => meeting.Name; return Title(); }", TestName = "A local function")]
|
||||
[TestCase("public string Label => meeting.Title();", TestName = "An extension property")]
|
||||
[TestCase("public static Meeting Create() => new(); public static Meeting Again() => Create();", TestName = "A static member of the block")]
|
||||
public async Task AMemberOfAnExtensionBlockIsLeftAlone(string member)
|
||||
{
|
||||
var source =
|
||||
$$"""
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Meeting
|
||||
{
|
||||
public string Name { get; set; } = "Kickoff";
|
||||
}
|
||||
|
||||
public static class MeetingExtensions
|
||||
{
|
||||
private const string SUFFIX = ".";
|
||||
|
||||
private static readonly string PREFIX = "Meeting: ";
|
||||
|
||||
private static string TB(string fallbackEN) => fallbackEN;
|
||||
|
||||
extension(Meeting meeting)
|
||||
{
|
||||
public string Title() => meeting.Name;
|
||||
|
||||
{{member}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var reported = await CompilationHarness.AnalyzeAsync(source, new ThisUsageAnalyzer());
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<Diagnostic>> AnalyzeClassAsync(string expression)
|
||||
{
|
||||
var source =
|
||||
$$"""
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Meeting
|
||||
{
|
||||
private const string ROOM = "A";
|
||||
|
||||
private static readonly string CITY = "Cologne";
|
||||
|
||||
private readonly string location = "Town hall";
|
||||
|
||||
public string Name { get; set; } = "Kickoff";
|
||||
|
||||
public string Run() => {{expression}};
|
||||
|
||||
private string Describe() => this.Name;
|
||||
|
||||
private static string Shorten(string text) => text[..1];
|
||||
}
|
||||
""";
|
||||
|
||||
return await CompilationHarness.AnalyzeAsync(source, new ThisUsageAnalyzer());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user