2026-11-04 10:46:18 +01:00
using AIStudio.Tests.Models.Generation ;
using Microsoft.CodeAnalysis ;
using SourceCodeRules.UsageAnalyzers ;
namespace AIStudio.Tests.Analyzers ;
/// <summary>
2026-11-08 21:10:51 +01:00
/// Checks that the texts of the app are looked up only through a T, TB, or TA wrapper, whose fallback text the I18N
2026-11-04 10:46:18 +01:00
/// collector finds.
/// </summary>
/// <remarks>
2026-11-08 21:10:51 +01:00
/// The collector reads the texts from calls of T, TB, and TA. A text handed to GetText anywhere else never reaches
2026-11-04 10:46:18 +01:00
/// 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")]
2026-11-08 21:10:51 +01:00
[TestCase(EXTENSION_BLOCK_FORM, "TA", TestName = "A TA wrapper around the member of an extension block")]
2026-11-04 10:46:18 +01:00
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")]
2026-11-08 21:10:51 +01:00
[TestCase(EXTENSION_BLOCK_FORM, """public string TA(string fallbackEN) => this.GetText("Hello");""", TestName = "The member of an extension block called by a TA wrapper with a text of its own")]
2026-11-04 10:46:18 +01:00
[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 ());
}
}