diff --git a/src/BLite.SourceGenerators/Helpers/BLiteConventions.cs b/src/BLite.SourceGenerators/Helpers/BLiteConventions.cs index 8b0a66f9..081ec6e3 100644 --- a/src/BLite.SourceGenerators/Helpers/BLiteConventions.cs +++ b/src/BLite.SourceGenerators/Helpers/BLiteConventions.cs @@ -9,8 +9,6 @@ internal static class BLiteConventions // ── DbContext discovery ──────────────────────────────────────────────── /// Base class name that marks a user class as a BLite DbContext. public const string DocumentDbContextBaseName = "DocumentDbContext"; - /// Suffix expected on DbContext class names to filter candidates early. - public const string DbContextClassSuffix = "Context"; /// File suffix used to skip already-generated files. public const string GeneratedFileSuffix = ".g.cs"; diff --git a/src/BLite.SourceGenerators/MapperGenerator.cs b/src/BLite.SourceGenerators/MapperGenerator.cs index 7e7cfa7f..45cd94d6 100644 --- a/src/BLite.SourceGenerators/MapperGenerator.cs +++ b/src/BLite.SourceGenerators/MapperGenerator.cs @@ -489,13 +489,18 @@ private static void PrintNestedTypes(StringBuilder sb, Dictionary + /// Syntactic pre-filter: any class with a base list. Whether it really derives from + /// DocumentDbContext is decided semantically in . + /// The class name is deliberately not part of the filter: a context may be called + /// anything (Shop, AppDb), and a name filter would skip it silently (#300). + /// private static bool IsPotentialDbContext(SyntaxNode node) { if (node.SyntaxTree.FilePath.EndsWith(BLiteConventions.GeneratedFileSuffix)) return false; return node is ClassDeclarationSyntax classDecl && - classDecl.BaseList != null && - classDecl.Identifier.Text.EndsWith(BLiteConventions.DbContextClassSuffix); + classDecl.BaseList != null; } // ── Interceptor helpers ─────────────────────────────────────────────────── diff --git a/tests/BLite.Shared/UnsuffixedNameFixtures.cs b/tests/BLite.Shared/UnsuffixedNameFixtures.cs new file mode 100644 index 00000000..a361113e --- /dev/null +++ b/tests/BLite.Shared/UnsuffixedNameFixtures.cs @@ -0,0 +1,44 @@ +using BLite.Bson; +using BLite.Core; +using BLite.Core.Collections; + +namespace BLite.Shared; + +// Regression fixtures for EntglDb/BLite#300: the mapper generator only looked at classes +// whose name ends in "Context", so a DocumentDbContext called anything else got no generated +// code and no diagnostic, and its collections stayed null. These classes compiling at all +// (InitializeCollections is generated) already proves the generator found them. + +/// A context whose name does not end in "Context". +public partial class Shop : DocumentDbContext +{ + public DocumentCollection Customers { get; set; } = null!; + + public Shop(string databasePath) : base(databasePath) + { + InitializeCollections(); + } +} + +/// A context that derives from another context, neither named with the suffix. +public partial class OnlineShop : Shop +{ + public DocumentCollection Orders { get; set; } = null!; + + public OnlineShop(string databasePath) : base(databasePath) + { + InitializeCollections(); + } +} + +public class ShopCustomer +{ + public ObjectId Id { get; set; } + public string Name { get; set; } = ""; +} + +public class ShopOrder +{ + public ObjectId Id { get; set; } + public decimal Total { get; set; } +} diff --git a/tests/BLite.Tests/UnsuffixedNameGeneratorTests.cs b/tests/BLite.Tests/UnsuffixedNameGeneratorTests.cs new file mode 100644 index 00000000..cd478bde --- /dev/null +++ b/tests/BLite.Tests/UnsuffixedNameGeneratorTests.cs @@ -0,0 +1,69 @@ +using BLite.Bson; +using BLite.Shared; + +namespace BLite.Tests; + +/// +/// Regression coverage for a DocumentDbContext whose class name does not end in +/// "Context" (see UnsuffixedNameFixtures.cs in BLite.Shared). The generator's syntax +/// filter used to require that suffix, so such a context got no generated code and no +/// diagnostic; its collection properties stayed null (EntglDb/BLite#300). +/// +public class UnsuffixedNameGeneratorTests : IDisposable +{ + private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"blite_unsuffixed_{Guid.NewGuid():N}.db"); + + public void Dispose() + { + TryDelete(_dbPath); + TryDelete(Path.ChangeExtension(_dbPath, ".wal")); + } + + private static void TryDelete(string path) + { + try { if (File.Exists(path)) File.Delete(path); } catch { /* best-effort */ } + } + + [Fact] + public async Task Context_NamedWithoutSuffix_HasGeneratedCollections_AndRoundTrips() + { + using var db = new Shop(_dbPath); + + Assert.NotNull(db.Customers); + + var id = ObjectId.NewObjectId(); + await db.Customers.InsertAsync(new ShopCustomer { Id = id, Name = "Ada" }); + + var stored = await db.Customers.FindByIdAsync(id); + Assert.NotNull(stored); + Assert.Equal("Ada", stored.Name); + } + + [Fact] + public async Task DerivedContext_NamedWithoutSuffix_HasBothCollections() + { + using var db = new OnlineShop(_dbPath); + + Assert.NotNull(db.Customers); + Assert.NotNull(db.Orders); + + var id = ObjectId.NewObjectId(); + await db.Orders.InsertAsync(new ShopOrder { Id = id, Total = 12.5m }); + var stored = await db.Orders.FindByIdAsync(id); + Assert.NotNull(stored); + Assert.Equal(12.5m, stored.Total); + } + + [Fact] + public void Generated_Mappers_Exist_ForTheUnsuffixedContext() + { + var mapperTypes = typeof(Shop).Assembly + .GetTypes() + // "{namespace}.{class}_{file}_Mappers", as for every context (see GlobalNamespaceGeneratorTests). + .Where(t => t.Namespace == $"{typeof(Shop).Namespace}.{nameof(Shop)}_UnsuffixedNameFixtures_Mappers") + .ToArray(); + + Assert.NotEmpty(mapperTypes); + Assert.Contains(mapperTypes, t => t.Name.Contains(nameof(ShopCustomer))); + } +}