using AIStudio.Tests.Models.Generation; using Microsoft.CodeAnalysis; using SourceCodeRules.UsageAnalyzers; namespace AIStudio.Tests.Analyzers; /// /// Checks which members MWAIS0005 wants to be reached through this., and that extension methods are never /// asked for it. /// /// /// An extension member has no this: 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 this. which nobody can write, and the tests for extension blocks fail. /// [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 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> 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()); } }