diff --git a/CLAUDE.md b/CLAUDE.md index 63ebc5f..a5200dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ ExpressiveSharp.EntityFrameworkCore.CodeFixers (Roslyn analyzer, netstandard2.0) ### Diagnostics -22 diagnostic codes (EXP0001–EXP0012, EXP0018 in `src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs`, EXP0013 in CodeFixers, EXP0014–EXP0020 for `[ExpressiveFor]` validation, EXP0036/EXP0037 in `WindowFunctionLiteralArgsAnalyzer`). Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0018 (unsupported operation ignored, e.g. alignment specifiers), EXP0019 (`[ExpressiveFor]` conflicts with `[Expressive]`), EXP0036 (`Ntile` non-positive literal), EXP0037 (`Lag`/`Lead` negative literal offset). +34 diagnostic codes: a contiguous `EXP0001–EXP0031` plus the migration band `EXP1001–EXP1003`. Generator diagnostics live in `src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs` (`EXP0001–EXP0012` core `[Expressive]`, `EXP0013–EXP0017` `[ExpressiveFor]`, `EXP0018–EXP0022` `[ExpressiveProperty]`, `EXP0023` ignored operation, `EXP0024` virtual member); analyzer diagnostics `EXP0025–EXP0029` in `ExpressiveSharp.CodeFixers`; window-function diagnostics `EXP0030`/`EXP0031` in `WindowFunctionLiteralArgsAnalyzer` (`EntityFrameworkCore.CodeFixers`); migration `EXP1001–EXP1003` in `MigrationAnalyzer`. The canonical reference is `docs/reference/diagnostics.md`. Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0023 (unsupported operation ignored, e.g. alignment specifiers), EXP0016 (`[ExpressiveFor]` conflicts with `[Expressive]`), EXP0030 (`Ntile` non-positive literal), EXP0031 (`Lag`/`Lead` negative literal offset). ## Testing diff --git a/docs/advanced/limitations.md b/docs/advanced/limitations.md index 1c7e44f..9de47e8 100644 --- a/docs/advanced/limitations.md +++ b/docs/advanced/limitations.md @@ -115,20 +115,20 @@ static bool IsNullOrWhiteSpace(string? s) Expression-tree expansion happens at **compile time** and works purely from the **static (declared) type** of each receiver. It has no runtime instance to inspect, so it cannot honor C# virtual dispatch. -If you mark a `virtual`, `abstract`, or `override` member `[Expressive]` (a default interface member counts too -- it is implicitly virtual), the generator reports [EXP0038](../reference/diagnostics#exp0038). When the member is expanded for a query provider (EF Core, MongoDB), the call is resolved against the declared type and the **base** body is always inlined -- an overridden body in a derived type is never used: +If you mark a `virtual`, `abstract`, or `override` member `[Expressive]` (a default interface member counts too -- it is implicitly virtual), the generator reports [EXP0024](../reference/diagnostics#exp0024). When the member is expanded for a query provider (EF Core, MongoDB), the call is resolved against the declared type and the **base** body is always inlined -- an overridden body in a derived type is never used: ```csharp public class Animal { public string Name { get; set; } = ""; - [Expressive] // EXP0038 + [Expressive] // EXP0024 public virtual string Describe() => $"Animal: {Name}"; } public class Dog : Animal { - [Expressive] // EXP0038 + [Expressive] // EXP0024 public override string Describe() => $"Dog: {Name}"; } @@ -152,7 +152,7 @@ db.Animals.AsExpressive().Select(a => a switch ### Recommended: use a non-virtual static/extension method -Move the logic into a single non-virtual `[Expressive]` method that performs the type test itself. This keeps the polymorphic shape in one place and produces no EXP0038: +Move the logic into a single non-virtual `[Expressive]` method that performs the type test itself. This keeps the polymorphic shape in one place and produces no EXP0024: ```csharp public static class AnimalExpressions @@ -169,7 +169,7 @@ db.Animals.AsExpressive().Select(a => a.Describe()); ``` ::: tip -Declaring entity members `virtual` is common in EF Core because it enables lazy-loading proxies. That remains fine for plain navigation and scalar properties -- EXP0038 only concerns members you *also* mark `[Expressive]`. +Declaring entity members `virtual` is common in EF Core because it enables lazy-loading proxies. That remains fine for plain navigation and scalar properties -- EXP0024 only concerns members you *also* mark `[Expressive]`. ::: ## Performance: First-Execution Overhead diff --git a/docs/guide/window-functions.md b/docs/guide/window-functions.md index 52fa9ac..392e318 100644 --- a/docs/guide/window-functions.md +++ b/docs/guide/window-functions.md @@ -306,7 +306,7 @@ The package was previously labeled experimental. Upgrading is API-compatible; th - Direct invocation of a `WindowFunction.*` stub (i.e. outside an EF Core query) now throws an exception that names the method and points at this guide. - `Ntile(0)` / `Ntile(-1)`, negative literal `Lag`/`Lead` offsets, and `NthValue(0)` now throw `InvalidOperationException` at translation time. Previously these reached the database and produced a provider-specific error. -- New analyzer warnings **EXP0036** (`Ntile` non-positive literal buckets) and **EXP0037** (`Lag`/`Lead` negative literal offsets) may surface on existing code. +- New analyzer warnings **EXP0030** (`Ntile` non-positive literal buckets) and **EXP0031** (`Lag`/`Lead` negative literal offsets) may surface on existing code. ## Next Steps diff --git a/docs/index.md b/docs/index.md index 550eb3e..791d3c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -55,7 +55,7 @@ features: - icon: "\U0001FA7A" title: Roslyn Analyzers & Code Fixes - details: EXP0001–EXP0036 diagnostics catch projection errors at compile time. Quick-fix actions and migration fixers from Projectables included. + details: EXP0001–EXP0031 diagnostics catch projection errors at compile time. Quick-fix actions and migration fixers from Projectables included. --- ## At a Glance diff --git a/docs/recipes/external-member-mapping.md b/docs/recipes/external-member-mapping.md index 84c81da..1808ff4 100644 --- a/docs/recipes/external-member-mapping.md +++ b/docs/recipes/external-member-mapping.md @@ -12,7 +12,7 @@ Use `[ExpressiveFor]` when: - You want to override how a specific member translates to SQL ::: info -If a member already has `[Expressive]`, adding `[ExpressiveFor]` targeting it is a compile error (EXP0019). `[ExpressiveFor]` is specifically for members that **do not** have `[Expressive]`. +If a member already has `[Expressive]`, adding `[ExpressiveFor]` targeting it is a compile error (EXP0016). `[ExpressiveFor]` is specifically for members that **do not** have `[Expressive]`. ::: ## Static Method: `Math.Clamp` @@ -189,11 +189,11 @@ static class DateTimeMappings | Code | Description | |------|-------------| -| EXP0014 | `[ExpressiveFor]` target type not found | -| EXP0015 | `[ExpressiveFor]` target member not found on the specified type | -| EXP0017 | Return type of stub does not match target member's return type | -| EXP0019 | Target member already has `[Expressive]` -- use `[Expressive]` directly instead | -| EXP0020 | Duplicate `[ExpressiveFor]` mapping for the same target member | +| EXP0013 | `[ExpressiveFor]` target type not found | +| EXP0014 | `[ExpressiveFor]` target member not found on the specified type | +| EXP0015 | Return type of stub does not match target member's return type | +| EXP0016 | Target member already has `[Expressive]` -- use `[Expressive]` directly instead | +| EXP0017 | Duplicate `[ExpressiveFor]` mapping for the same target member | ## Tips diff --git a/docs/reference/diagnostics.md b/docs/reference/diagnostics.md index 9279c8a..ae6db68 100644 --- a/docs/reference/diagnostics.md +++ b/docs/reference/diagnostics.md @@ -6,10 +6,6 @@ The ExpressiveSharp source generator and companion analyzers emit diagnostics du See [Troubleshooting](./troubleshooting) for symptom-oriented guidance -- find the error message or behavior you see and get step-by-step resolution. ::: -::: info Retired diagnostics -`EXP0016` ("`[ExpressiveFor]` stub must be static") has been retired. Instance stubs on the target type are now permitted; constructor-stub and unrelated-type mismatches surface as `EXP0015` (member not found) instead. -::: - ## Overview | ID | Severity | Title | Code Fix | @@ -26,20 +22,25 @@ See [Troubleshooting](./troubleshooting) for symptom-oriented guidance -- find t | [EXP0010](#exp0010) | Warning | Interceptor emission failed | -- | | [EXP0011](#exp0011) | Warning | Unresolvable member in pattern | -- | | [EXP0012](#exp0012) | Info | Factory method can be converted to constructor | -- | -| [EXP0013](#exp0013) | Warning | Referenced member could benefit from `[Expressive]` | [Add `[Expressive]`](#exp0013-fix) | -| [EXP0014](#exp0014) | Error | `[ExpressiveFor]` target type not found | -- | -| [EXP0015](#exp0015) | Error | `[ExpressiveFor]` target member not found | -- | -| [EXP0017](#exp0017) | Error | `[ExpressiveFor]` return type mismatch | -- | -| [EXP0019](#exp0019) | Error | `[ExpressiveFor]` conflicts with `[Expressive]` | -- | -| [EXP0020](#exp0020) | Error | Duplicate `[ExpressiveFor]` mapping | -- | -| [EXP0027](#exp0027) | Info | Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()` | [Wrap with `.AsExpressive()`](#exp0027-fix) | -| [EXP0031](#exp0031) | Error | `[ExpressiveProperty]` target name is already defined | -- | -| [EXP0032](#exp0032) | Error | `[ExpressiveProperty]` requires a partial containing type | -- | -| [EXP0033](#exp0033) | Error | `[ExpressiveProperty]` requires an expression-bodied property stub | -- | -| [EXP0034](#exp0034) | Error | `[ExpressiveProperty]` requires an instance stub | -- | -| [EXP0035](#exp0035) | Error | `[ExpressiveProperty]` target shadows inherited member | -- | -| [EXP0036](#exp0036) | Info | `IExpressiveQueryable` chain dropped to plain `IQueryable` | -- | -| [EXP0038](#exp0038) | Warning | `[Expressive]` member is virtual and will not dispatch polymorphically | -- | +| [EXP0013](#exp0013) | Error | `[ExpressiveFor]` target type not found | -- | +| [EXP0014](#exp0014) | Error | `[ExpressiveFor]` target member not found | -- | +| [EXP0015](#exp0015) | Error | `[ExpressiveFor]` return type mismatch | -- | +| [EXP0016](#exp0016) | Error | `[ExpressiveFor]` conflicts with `[Expressive]` | -- | +| [EXP0017](#exp0017) | Error | Duplicate `[ExpressiveFor]` mapping | -- | +| [EXP0018](#exp0018) | Error | `[ExpressiveProperty]` target name is already defined | -- | +| [EXP0019](#exp0019) | Error | `[ExpressiveProperty]` requires a partial containing type | -- | +| [EXP0020](#exp0020) | Error | `[ExpressiveProperty]` requires an expression-bodied property stub | -- | +| [EXP0021](#exp0021) | Error | `[ExpressiveProperty]` requires an instance stub | -- | +| [EXP0022](#exp0022) | Error | `[ExpressiveProperty]` target shadows inherited member | -- | +| [EXP0023](#exp0023) | Warning | Unsupported operation ignored | -- | +| [EXP0024](#exp0024) | Warning | `[Expressive]` member is virtual and will not dispatch polymorphically | -- | +| [EXP0025](#exp0025) | Warning | Referenced member could benefit from `[Expressive]` | [Add `[Expressive]`](#exp0025-fix) | +| [EXP0026](#exp0026) | Warning | `IExpressiveQueryable` LINQ method resolves to `Queryable` | [Add `using ExpressiveSharp;`](#exp0026-fix) | +| [EXP0027](#exp0027) | Info | No `IExpressiveQueryable` overload for `Queryable` method | -- | +| [EXP0028](#exp0028) | Info | Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()` | [Wrap with `.AsExpressive()`](#exp0028-fix) | +| [EXP0029](#exp0029) | Info | `IExpressiveQueryable` chain dropped to plain `IQueryable` | -- | +| [EXP0030](#exp0030) | Warning | `WindowFunction.Ntile` requires a positive bucket count | -- | +| [EXP0031](#exp0031) | Warning | `WindowFunction.Lag`/`Lead` offset must be non-negative | -- | | [EXP1001](#exp1001) | Warning | Replace `[Projectable]` with `[Expressive]` | [Replace attribute](#exp1001-fix) | | [EXP1002](#exp1002) | Warning | Replace `UseProjectables()` with `UseExpressives()` | [Replace method call](#exp1002-fix) | | [EXP1003](#exp1003) | Warning | Replace Projectables namespace | [Replace namespace](#exp1003-fix) | @@ -337,46 +338,11 @@ public CustomerDto(Customer c) --- -## Analyzer Diagnostic (EXP0013) - -### EXP0013 -- Referenced member could benefit from `[Expressive]` {#exp0013} - -**Severity:** Warning -**Category:** Design -**Source:** `MissingExpressiveAnalyzer` (in `ExpressiveSharp.CodeFixers`) - -**Message:** -``` -Member '{0}' is referenced in an [Expressive] expression but is not marked [Expressive]. -Adding [Expressive] would allow its body to be inlined into the expression tree. -``` - -**Cause:** A member referenced inside an `[Expressive]` body, an `ExpressionPolyfill.Create()` lambda, or an `IExpressiveQueryable` LINQ lambda has an expandable body (expression-bodied or block-bodied) but is not marked `[Expressive]`. Without the attribute, the member call remains opaque in the generated expression tree and cannot be translated by LINQ providers. - -**Fix:** {#exp0013-fix} - -The IDE offers a code fix that adds `[Expressive]` to the referenced member automatically (including the `using ExpressiveSharp;` directive if needed): - -```csharp -// Warning: Total is referenced in an [Expressive] body but not marked [Expressive] -public double Total => Price * Quantity; - -// Fixed: add [Expressive] -[Expressive] -public double Total => Price * Quantity; -``` - -::: tip -Enum method calls are excluded from this diagnostic -- the generator expands those automatically via per-value ternary chains, so `[Expressive]` is not needed on the enum extension method. -::: - ---- - -## External Mapping Diagnostics (EXP0014--EXP0020) +## External Mapping Diagnostics (EXP0013--EXP0017) These diagnostics are specific to `[ExpressiveFor]` and `[ExpressiveForConstructor]`. See [`[ExpressiveFor]` Mapping](./expressive-for) for full usage details. -### EXP0014 -- Target type not found {#exp0014} +### EXP0013 -- Target type not found {#exp0013} **Severity:** Error **Category:** Design @@ -392,7 +358,7 @@ These diagnostics are specific to `[ExpressiveFor]` and `[ExpressiveForConstruct --- -### EXP0015 -- Target member not found {#exp0015} +### EXP0014 -- Target member not found {#exp0014} **Severity:** Error **Category:** Design @@ -420,7 +386,7 @@ static double Clamp(double value, double min, double max) --- -### EXP0017 -- Return type mismatch {#exp0017} +### EXP0015 -- Return type mismatch {#exp0015} **Severity:** Error **Category:** Design @@ -446,7 +412,7 @@ static double Clamp(double value, double min, double max) => /* ... */; --- -### EXP0019 -- Conflicts with `[Expressive]` {#exp0019} +### EXP0016 -- Conflicts with `[Expressive]` {#exp0016} **Severity:** Error **Category:** Design @@ -462,7 +428,7 @@ Target member '{0}' on type '{1}' already has [Expressive]; remove [ExpressiveFo --- -### EXP0020 -- Duplicate mapping {#exp0020} +### EXP0017 -- Duplicate mapping {#exp0017} **Severity:** Error **Category:** Design @@ -478,48 +444,15 @@ Duplicate [ExpressiveFor] mapping for member '{0}' on type '{1}'; only one stub --- -### EXP0027 -- Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()` {#exp0027} - -**Severity:** Info -**Category:** Usage - -**Message:** -``` -LINQ method '{0}' on a plain IQueryable references the [Expressive] member '{1}'. -Without .AsExpressive(), the member's body will not be inlined into the expression tree; -the provider may evaluate the call in memory or fail to translate it. Wrap the source -with .AsExpressive(). -``` - -**Cause:** A LINQ method on a plain `IQueryable` receiver (one that is not `IExpressiveQueryable`) is invoked with a lambda whose body references an `[Expressive]` member. Because the chain is not expressive-aware, the source generator does not rewrite the lambda into an expression tree that inlines the member's body — the underlying query provider receives a call to the runtime delegate. Most providers cannot translate this and will either evaluate the call client-side (silent overfetch) or throw at execution time. - -**Fix:** Wrap the chain root with `.AsExpressive()` so that subsequent LINQ methods flow through the ExpressiveSharp delegate-based overloads, which inline `[Expressive]` member bodies at compile time. - -```csharp -// Before — IsAdult is silently evaluated on the client. -var adults = users.Where(u => u.IsAdult).ToList(); - -// After — IsAdult is inlined into the expression tree before the provider sees it. -var adults = users.AsExpressive().Where(u => u.IsAdult).ToList(); -``` - -When you intentionally want to evaluate a member at runtime (e.g., it captures process state), mark the member with `[NotExpressive]` to suppress the diagnostic at every call site. - -#### Code Fix: Wrap source with `.AsExpressive()` {#exp0027-fix} - -The IDE offers a single code action: **Wrap source with `.AsExpressive()`**. It walks the LINQ chain to the leftmost non-LINQ expression, wraps it with `.AsExpressive()`, and inserts `using ExpressiveSharp;` if it is not already imported. - ---- - -## `[ExpressiveProperty]` Diagnostics (EXP0031--EXP0035) +## `[ExpressiveProperty]` Diagnostics (EXP0018--EXP0022) These diagnostics apply to `[ExpressiveProperty]` stubs, which ask the generator to emit a new property on the stub's containing partial type. See [`[ExpressiveProperty]` Attribute](./expressive-property) for the full feature reference. ::: info Replacing `[Expressive(Projectable = true)]` -`[ExpressiveProperty]` replaces the now-removed `[Expressive(Projectable = true)]`. Diagnostic codes `EXP0021`--`EXP0026` and `EXP0028`--`EXP0030` were retired along with that feature and are not reused. (EXP0027 has been reassigned to the [plain-IQueryable analyzer](#exp0027).) The migration recipe is in [Migration from Projectables](../guide/migration-from-projectables#migrating-usememberbody). +`[ExpressiveProperty]` replaces the now-removed `[Expressive(Projectable = true)]`. The migration recipe is in [Migration from Projectables](../guide/migration-from-projectables#migrating-usememberbody). ::: -### EXP0031 -- Target name is already defined {#exp0031} +### EXP0018 -- Target name is already defined {#exp0018} **Severity:** Error **Category:** Design @@ -548,7 +481,7 @@ private decimal AmountExpression => TotalAmount - Discount; --- -### EXP0032 -- Requires a partial containing type {#exp0032} +### EXP0019 -- Requires a partial containing type {#exp0019} **Severity:** Error **Category:** Design @@ -581,7 +514,7 @@ public partial class Account --- -### EXP0033 -- Requires an expression-bodied property stub {#exp0033} +### EXP0020 -- Requires an expression-bodied property stub {#exp0020} **Severity:** Error **Category:** Design @@ -612,7 +545,7 @@ private decimal AmountExpression => TotalAmount - Discount; --- -### EXP0034 -- Requires an instance stub {#exp0034} +### EXP0021 -- Requires an instance stub {#exp0021} **Severity:** Error **Category:** Design @@ -629,7 +562,7 @@ an instance member --- -### EXP0035 -- Target shadows inherited member {#exp0035} +### EXP0022 -- Target shadows inherited member {#exp0022} **Severity:** Error **Category:** Design @@ -647,7 +580,180 @@ on an override --- -### EXP0036 -- `IExpressiveQueryable` chain dropped to plain `IQueryable` {#exp0036} +## General Diagnostics (EXP0023--EXP0024) + +### EXP0023 -- Unsupported operation ignored {#exp0023} + +**Severity:** Warning +**Category:** Design + +**Message:** +``` +Expression contains an unsupported operation ({0}). The operation will be ignored and the +surrounding expression emitted without it. +``` + +**Cause:** The member body contains an operation that has no expression-tree equivalent but can be dropped without substituting a value — most commonly a string-interpolation **alignment or format specifier** (e.g. `$"{value,10}"` or `$"{value:N2}"`). The generator emits the surrounding interpolated string but does not honor the specifier. + +**Fix:** If the formatting matters, compute the formatted value outside the `[Expressive]` body, or apply formatting in the consuming code after materialization. Unlike [EXP0008](#exp0008) — which substitutes a `default` value and can change results — `EXP0023` only drops the unsupported sub-operation; the surrounding expression is still emitted correctly. + +--- + +### EXP0024 -- Virtual member will not dispatch polymorphically {#exp0024} + +**Severity:** Warning +**Category:** Design + +**Message:** +``` +[Expressive] member '{0}' is virtual, abstract, or an override. When it is expanded into an +expression tree (e.g. for EF Core or MongoDB), the call is resolved using the static (declared) +type, so an overridden body in a derived type is never used. Test the runtime type explicitly +(e.g. 'x switch { Derived d => d.Member, _ => x.Member }'), or move the logic into a non-virtual +[Expressive] static/extension method. +``` + +**Cause:** An `[Expressive]` member is declared `virtual`, `abstract`, or `override` (a default interface member counts -- it is implicitly virtual). Expression-tree expansion happens at compile time and only sees the **static (declared) type** of the receiver, so it cannot honor C# virtual dispatch. When the member is expanded for a query provider, the **base** body is always inlined; an overridden body in a derived type is never used. + +This differs from compiling the expression to a delegate and invoking it in memory, where the CLR dispatches on the runtime type. + +**Fix:** Branch on the runtime type so each arm has a statically-typed receiver, or move the logic into a single non-virtual `[Expressive]` static/extension method. See [Limitations: virtual and polymorphic members](../advanced/limitations#virtual-polymorphic-members) for full examples. + +```csharp +// Warning: virtual [Expressive] member +[Expressive] +public virtual string Describe() => $"Animal: {Name}"; + +// Fix 1: test the runtime type explicitly +db.Animals.AsExpressive().Select(a => a switch +{ + Dog d => d.Describe(), + _ => a.Describe(), +}); + +// Fix 2: non-virtual [Expressive] extension method that does the type test once +[Expressive] +public static string Describe(this Animal a) => a switch +{ + Dog d => $"Dog: {d.Name}", + _ => $"Animal: {a.Name}", +}; +``` + +If a virtual member is intentional (you only ever compile it to an in-memory delegate, never translate it through a provider), suppress the warning with `#pragma warning disable EXP0024` or `$(NoWarn);EXP0024`. + +--- + +## Analyzer & `IExpressiveQueryable` Diagnostics (EXP0025--EXP0029) + +These diagnostics are emitted by the companion analyzers in `ExpressiveSharp.CodeFixers` (shipped with the `ExpressiveSharp` package). They flag places where `[Expressive]` rewriting silently won't apply. + +### EXP0025 -- Referenced member could benefit from `[Expressive]` {#exp0025} + +**Severity:** Warning +**Category:** Design +**Source:** `MissingExpressiveAnalyzer` (in `ExpressiveSharp.CodeFixers`) + +**Message:** +``` +Member '{0}' is referenced in an [Expressive] expression but is not marked [Expressive]. +Adding [Expressive] would allow its body to be inlined into the expression tree. +``` + +**Cause:** A member referenced inside an `[Expressive]` body, an `ExpressionPolyfill.Create()` lambda, or an `IExpressiveQueryable` LINQ lambda has an expandable body (expression-bodied or block-bodied) but is not marked `[Expressive]`. Without the attribute, the member call remains opaque in the generated expression tree and cannot be translated by LINQ providers. + +**Fix:** {#exp0025-fix} + +The IDE offers a code fix that adds `[Expressive]` to the referenced member automatically (including the `using ExpressiveSharp;` directive if needed): + +```csharp +// Warning: Total is referenced in an [Expressive] body but not marked [Expressive] +public double Total => Price * Quantity; + +// Fixed: add [Expressive] +[Expressive] +public double Total => Price * Quantity; +``` + +::: tip +Enum method calls are excluded from this diagnostic -- the generator expands those automatically via per-value ternary chains, so `[Expressive]` is not needed on the enum extension method. +::: + +--- + +### EXP0026 -- `IExpressiveQueryable` LINQ method resolves to `Queryable` {#exp0026} + +**Severity:** Warning +**Category:** Usage +**Source:** `MissingExpressiveImportAnalyzer` (in `ExpressiveSharp.CodeFixers`) + +**Message:** +``` +LINQ method '{0}' on IExpressiveQueryable resolves to System.Linq.Queryable instead of the +ExpressiveSharp overload. Add 'using ExpressiveSharp;' to enable expression tree rewriting and +maintain the IExpressiveQueryable chain. +``` + +**Cause:** A LINQ method invoked on an `IExpressiveQueryable` receiver bound to `System.Linq.Queryable` rather than the ExpressiveSharp delegate-based overload, because `using ExpressiveSharp;` is not imported. Without that import the chain silently degrades to plain `IQueryable` and `[Expressive]` members are no longer rewritten. + +**Fix:** {#exp0026-fix} + +Add `using ExpressiveSharp;`. The IDE offers a code fix that inserts the directive automatically. + +--- + +### EXP0027 -- No `IExpressiveQueryable` overload for `Queryable` method {#exp0027} + +**Severity:** Info +**Category:** Usage +**Source:** `MissingExpressiveImportAnalyzer` (in `ExpressiveSharp.CodeFixers`) + +**Message:** +``` +Method '{0}' from System.Linq.Queryable has no IExpressiveQueryable overload. The result +will be IQueryable, breaking the IExpressiveQueryable chain. +``` + +**Cause:** The called `Queryable` method has no ExpressiveSharp stub, so its result is plain `IQueryable` and the expressive chain ends here. Unlike [EXP0026](#exp0026), this is not fixable by adding an import — no overload exists. + +**Fix:** Re-establish the chain after the call with `.AsExpressive()` if you need `[Expressive]` rewriting downstream, or accept the plain `IQueryable` if the remainder of the query needs no rewriting. + +--- + +### EXP0028 -- Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()` {#exp0028} + +**Severity:** Info +**Category:** Usage + +**Message:** +``` +LINQ method '{0}' on a plain IQueryable references the [Expressive] member '{1}'. +Without .AsExpressive(), the member's body will not be inlined into the expression tree; +the provider may evaluate the call in memory or fail to translate it. Wrap the source +with .AsExpressive(). +``` + +**Cause:** A LINQ method on a plain `IQueryable` receiver (one that is not `IExpressiveQueryable`) is invoked with a lambda whose body references an `[Expressive]` member. Because the chain is not expressive-aware, the source generator does not rewrite the lambda into an expression tree that inlines the member's body — the underlying query provider receives a call to the runtime delegate. Most providers cannot translate this and will either evaluate the call client-side (silent overfetch) or throw at execution time. + +**Fix:** Wrap the chain root with `.AsExpressive()` so that subsequent LINQ methods flow through the ExpressiveSharp delegate-based overloads, which inline `[Expressive]` member bodies at compile time. + +```csharp +// Before — IsAdult is silently evaluated on the client. +var adults = users.Where(u => u.IsAdult).ToList(); + +// After — IsAdult is inlined into the expression tree before the provider sees it. +var adults = users.AsExpressive().Where(u => u.IsAdult).ToList(); +``` + +When you intentionally want to evaluate a member at runtime (e.g., it captures process state), mark the member with `[NotExpressive]` to suppress the diagnostic at every call site. + +#### Code Fix: Wrap source with `.AsExpressive()` {#exp0028-fix} + +The IDE offers a single code action: **Wrap source with `.AsExpressive()`**. It walks the LINQ chain to the leftmost non-LINQ expression, wraps it with `.AsExpressive()`, and inserts `using ExpressiveSharp;` if it is not already imported. + +--- + +### EXP0029 -- `IExpressiveQueryable` chain dropped to plain `IQueryable` {#exp0029} **Severity:** Info **Category:** Usage @@ -670,7 +776,7 @@ The diagnostic fires once, at the dropout point itself, regardless of how many f // User-defined helper typed on plain IQueryable — drops the chain. public static IQueryable Filter(this IQueryable source) => source.Where(...); -db.Orders.AsExpressiveDbSet().Filter() // ⚠ EXP0036 fires on .Filter() +db.Orders.AsExpressiveDbSet().Filter() // ⚠ EXP0029 fires on .Filter() .Include(o => o.Customer) // chain is plain from here on .ToList(); ``` @@ -698,50 +804,55 @@ db.Orders.AsExpressiveDbSet() --- -## Virtual Member Diagnostic (EXP0038) +## Window Function Diagnostics (EXP0030--EXP0031) + +These diagnostics are emitted by the `WindowFunctionLiteralArgsAnalyzer` in the `ExpressiveSharp.EntityFrameworkCore.CodeFixers` package (shipped with `ExpressiveSharp.EntityFrameworkCore`). They validate constant literal arguments to `WindowFunction.*` calls before they reach the database. Only compile-time constant arguments are checked; a variable count or offset is never flagged. See [Window Functions](../guide/window-functions) for the full feature reference. -### EXP0038 -- Virtual member will not dispatch polymorphically {#exp0038} +### EXP0030 -- `WindowFunction.Ntile` requires a positive bucket count {#exp0030} **Severity:** Warning -**Category:** Design +**Category:** Usage **Message:** ``` -[Expressive] member '{0}' is virtual, abstract, or an override. When it is expanded into an -expression tree (e.g. for EF Core or MongoDB), the call is resolved using the static (declared) -type, so an overridden body in a derived type is never used. Test the runtime type explicitly -(e.g. 'x switch { Derived d => d.Member, _ => x.Member }'), or move the logic into a non-virtual -[Expressive] static/extension method. +WindowFunction.Ntile requires a positive bucket count; literal value {0} produces invalid SQL ``` -**Cause:** An `[Expressive]` member is declared `virtual`, `abstract`, or `override` (a default interface member counts -- it is implicitly virtual). Expression-tree expansion happens at compile time and only sees the **static (declared) type** of the receiver, so it cannot honor C# virtual dispatch. When the member is expanded for a query provider, the **base** body is always inlined; an overridden body in a derived type is never used. - -This differs from compiling the expression to a delegate and invoking it in memory, where the CLR dispatches on the runtime type. +**Cause:** `NTILE(n)` divides ordered rows into `n` buckets, so SQL requires `n >= 1`. A literal `0` or negative bucket count raises a database error at execution time. -**Fix:** Branch on the runtime type so each arm has a statically-typed receiver, or move the logic into a single non-virtual `[Expressive]` static/extension method. See [Limitations: virtual and polymorphic members](../advanced/limitations#virtual-polymorphic-members) for full examples. +**Fix:** Pass a positive literal bucket count: ```csharp -// Warning: virtual [Expressive] member -[Expressive] -public virtual string Describe() => $"Animal: {Name}"; +// Warning: EXP0030 +WindowFunction.Ntile(0, Window.OrderBy(x => x.Score)); -// Fix 1: test the runtime type explicitly -db.Animals.AsExpressive().Select(a => a switch -{ - Dog d => d.Describe(), - _ => a.Describe(), -}); +// Fixed +WindowFunction.Ntile(4, Window.OrderBy(x => x.Score)); +``` -// Fix 2: non-virtual [Expressive] extension method that does the type test once -[Expressive] -public static string Describe(this Animal a) => a switch -{ - Dog d => $"Dog: {d.Name}", - _ => $"Animal: {a.Name}", -}; +--- + +### EXP0031 -- `WindowFunction.Lag`/`Lead` offset must be non-negative {#exp0031} + +**Severity:** Warning +**Category:** Usage + +**Message:** ``` +WindowFunction.{0} offset must be non-negative; literal value {1} is rejected during EF translation +``` + +**Cause:** `LAG` and `LEAD` offsets count rows backward or forward from the current row; SQL requires the offset to be `>= 0`. A negative literal offset is rejected during EF translation. -If a virtual member is intentional (you only ever compile it to an in-memory delegate, never translate it through a provider), suppress the warning with `#pragma warning disable EXP0038` or `$(NoWarn);EXP0038`. +**Fix:** Use a non-negative offset — swap `Lag` for `Lead` (or vice versa) instead of negating: + +```csharp +// Warning: EXP0031 +WindowFunction.Lag(x.Value, -1, window); + +// Fixed +WindowFunction.Lead(x.Value, 1, window); +``` --- diff --git a/docs/reference/expressive-attribute.md b/docs/reference/expressive-attribute.md index 30d1987..b258697 100644 --- a/docs/reference/expressive-attribute.md +++ b/docs/reference/expressive-attribute.md @@ -140,8 +140,9 @@ expr.ExpandExpressives(); // RemoveNullConditionalPatterns applied automatically Use `[NotExpressive]` to mark a member that *looks* expressive-eligible (it has an expression body that the source generator could lift) but should intentionally remain runtime-evaluated. The attribute suppresses the analyzer suggestions: -- [EXP0013](./diagnostics#exp0013) — "Member could benefit from `[Expressive]`" -- [EXP0027](./diagnostics#exp0027) — "Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()`" +- [EXP0025](./diagnostics#exp0025) — "Member could benefit from `[Expressive]`" +- [EXP0028](./diagnostics#exp0028) — "Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()`" +- [EXP0029](./diagnostics#exp0029) — "`IExpressiveQueryable` chain dropped to plain `IQueryable`" (when applied to the method that drops the chain) ```csharp public class Order diff --git a/docs/reference/expressive-for.md b/docs/reference/expressive-for.md index 118f433..0e5740e 100644 --- a/docs/reference/expressive-for.md +++ b/docs/reference/expressive-for.md @@ -23,7 +23,7 @@ You write a stub member -- a method **or** a property -- whose body defines the - For **instance properties** with an `instance` method or property stub on the target type, the stub is parameterless. - For **static properties**, the stub is parameterless. - Property stubs can only target other properties (no parameters to carry method arguments). -- The return type / property type must match (EXP0017 if not). +- The return type / property type must match (EXP0015 if not). - Constructor stubs (`[ExpressiveForConstructor]`) must still be `static` methods; instance or property ctor stubs have no coherent meaning. ## Static Method Mapping @@ -167,14 +167,14 @@ The following diagnostics are specific to `[ExpressiveFor]` and `[ExpressiveForC | Code | Severity | Description | |------|----------|-------------| -| [EXP0014](./diagnostics#exp0014) | Error | Target type specified in `[ExpressiveFor]` could not be resolved | -| [EXP0015](./diagnostics#exp0015) | Error | No member with the given name found on the target type matching the stub's parameter signature | -| [EXP0017](./diagnostics#exp0017) | Error | Return type of the stub does not match the target member's return type | -| [EXP0019](./diagnostics#exp0019) | Error | The target member already has `[Expressive]` -- remove one of the two attributes | -| [EXP0020](./diagnostics#exp0020) | Error | Duplicate mapping -- only one stub per target member is allowed | +| [EXP0013](./diagnostics#exp0013) | Error | Target type specified in `[ExpressiveFor]` could not be resolved | +| [EXP0014](./diagnostics#exp0014) | Error | No member with the given name found on the target type matching the stub's parameter signature | +| [EXP0015](./diagnostics#exp0015) | Error | Return type of the stub does not match the target member's return type | +| [EXP0016](./diagnostics#exp0016) | Error | The target member already has `[Expressive]` -- remove one of the two attributes | +| [EXP0017](./diagnostics#exp0017) | Error | Duplicate mapping -- only one stub per target member is allowed | ::: warning -If a member already has `[Expressive]`, adding `[ExpressiveFor]` targeting it is a compile error (EXP0019). `[ExpressiveFor]` is only for members that do not have `[Expressive]`. +If a member already has `[Expressive]`, adding `[ExpressiveFor]` targeting it is a compile error (EXP0016). `[ExpressiveFor]` is only for members that do not have `[Expressive]`. ::: ## Complete Usage Example diff --git a/docs/reference/expressive-property.md b/docs/reference/expressive-property.md index 54703b6..6a3c58f 100644 --- a/docs/reference/expressive-property.md +++ b/docs/reference/expressive-property.md @@ -91,11 +91,11 @@ If you need a mutable `set` (not just `init`), that's a future addition; open an | Code | Cause | |------|-------| -| [EXP0031](./diagnostics#exp0031) | Target name already defined on the containing type. Rename the stub, or switch to `[ExpressiveFor(nameof(X))]`. | -| [EXP0032](./diagnostics#exp0032) | Containing type is not `partial`. | -| [EXP0033](./diagnostics#exp0033) | Stub is not a property with top-level expression body. | -| [EXP0034](./diagnostics#exp0034) | Stub is `static`. | -| [EXP0035](./diagnostics#exp0035) | Target name shadows an inherited member on a base type. | +| [EXP0018](./diagnostics#exp0018) | Target name already defined on the containing type. Rename the stub, or switch to `[ExpressiveFor(nameof(X))]`. | +| [EXP0019](./diagnostics#exp0019) | Containing type is not `partial`. | +| [EXP0020](./diagnostics#exp0020) | Stub is not a property with top-level expression body. | +| [EXP0021](./diagnostics#exp0021) | Stub is `static`. | +| [EXP0022](./diagnostics#exp0022) | Target name shadows an inherited member on a base type. | ## EF Core and MongoDB integration diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index 663ffe0..357038a 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -103,7 +103,7 @@ These are **warnings**, not errors. The generated code compiles but substitutes --- -### "Member could benefit from `[Expressive]`" (EXP0013) {#exp0013} +### "Member could benefit from `[Expressive]`" (EXP0025) {#exp0025} **Symptom:** A member referenced inside an `[Expressive]` body is not itself marked `[Expressive]`, producing a warning. This means the member's body is opaque to the expression tree -- it will be called as a delegate instead of being inlined. @@ -160,7 +160,7 @@ static bool IsNullOrWhiteSpace(string? s) **2. Referenced member not marked `[Expressive]`** -If an `[Expressive]` member references another member that is *not* `[Expressive]`, the referenced member remains opaque to EF Core. Look for EXP0013 warnings and add `[Expressive]` to the referenced member. +If an `[Expressive]` member references another member that is *not* `[Expressive]`, the referenced member remains opaque to EF Core. Look for EXP0025 warnings and add `[Expressive]` to the referenced member. **3. Unsupported operation in the body** diff --git a/src/ExpressiveSharp.CodeFixers/AddExpressiveCodeFixProvider.cs b/src/ExpressiveSharp.CodeFixers/AddExpressiveCodeFixProvider.cs index 82683fe..209bc98 100644 --- a/src/ExpressiveSharp.CodeFixers/AddExpressiveCodeFixProvider.cs +++ b/src/ExpressiveSharp.CodeFixers/AddExpressiveCodeFixProvider.cs @@ -12,7 +12,7 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Code fix for EXP0013: adds [Expressive] to the referenced member's declaration. +/// Code fix for EXP0025: adds [Expressive] to the referenced member's declaration. /// The diagnostic's additional location (index 0) points to that declaration. /// [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddExpressiveCodeFixProvider))] @@ -20,7 +20,7 @@ namespace ExpressiveSharp.CodeFixers; public sealed class AddExpressiveCodeFixProvider : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds => - ImmutableArray.Create("EXP0013"); + ImmutableArray.Create("EXP0025"); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -40,7 +40,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) CodeAction.Create( title: "Add [Expressive] attribute", createChangedSolution: ct => AddExpressiveAttributeAsync(declDocument, declLocation, ct), - equivalenceKey: "EXP0013_AddExpressive"), + equivalenceKey: "EXP0025_AddExpressive"), diagnostic); } } diff --git a/src/ExpressiveSharp.CodeFixers/AddExpressiveUsingCodeFixProvider.cs b/src/ExpressiveSharp.CodeFixers/AddExpressiveUsingCodeFixProvider.cs index 205edeb..d22252f 100644 --- a/src/ExpressiveSharp.CodeFixers/AddExpressiveUsingCodeFixProvider.cs +++ b/src/ExpressiveSharp.CodeFixers/AddExpressiveUsingCodeFixProvider.cs @@ -11,14 +11,14 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Provides a code fix for EXP0021 that adds using ExpressiveSharp; to bring +/// Provides a code fix for EXP0026 that adds using ExpressiveSharp; to bring /// the delegate-based LINQ stubs into scope. /// [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddExpressiveUsingCodeFixProvider))] public sealed class AddExpressiveUsingCodeFixProvider : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds => - ImmutableArray.Create("EXP0021"); + ImmutableArray.Create("EXP0026"); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -37,7 +37,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) title: "Add 'using ExpressiveSharp'", createChangedDocument: _ => Task.FromResult( context.Document.WithSyntaxRoot(EnsureUsingDirective(root))), - equivalenceKey: "EXP0021_AddUsing"), + equivalenceKey: "EXP0026_AddUsing"), diagnostic); } } diff --git a/src/ExpressiveSharp.CodeFixers/ExpressiveQueryableDropoutAnalyzer.cs b/src/ExpressiveSharp.CodeFixers/ExpressiveQueryableDropoutAnalyzer.cs index b47a7d6..6a17f5a 100644 --- a/src/ExpressiveSharp.CodeFixers/ExpressiveQueryableDropoutAnalyzer.cs +++ b/src/ExpressiveSharp.CodeFixers/ExpressiveQueryableDropoutAnalyzer.cs @@ -8,7 +8,7 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Reports EXP0036 at the *dropout call* itself: a method invocation whose receiver implements +/// Reports EXP0029 at the *dropout call* itself: a method invocation whose receiver implements /// IExpressiveQueryable<T> but whose return type is plain IQueryable<T>. /// The chain stops being expressive at this call; downstream LINQ skips ExpressiveSharp rewriting. /// @@ -23,7 +23,7 @@ namespace ExpressiveSharp.CodeFixers; public sealed class ExpressiveQueryableDropoutAnalyzer : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor ExpressiveQueryableDropout = new( - id: "EXP0036", + id: "EXP0029", title: "IExpressiveQueryable chain dropped to plain IQueryable", messageFormat: "'{0}' returns IQueryable from an IExpressiveQueryable receiver, dropping the expressive chain. Downstream LINQ skips ExpressiveSharp rewriting and [Expressive] members may evaluate on the client. Add an IExpressiveQueryable-typed overload of '{0}', wrap the result with .AsExpressive(), or mark the method [NotExpressive] if the dropout is intentional.", category: "Usage", @@ -65,7 +65,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT if (symbolInfo.Symbol is not IMethodSymbol method) return; - // Method-level opt-out. Honored same as EXP0027 honors it on referenced members. + // Method-level opt-out. Honored same as EXP0028 honors it on referenced members. if (ExpressiveSymbolHelpers.HasNotExpressiveAttribute(method)) return; @@ -89,7 +89,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT return; // Suppress when an IExpressiveQueryable sibling exists in any referenced namespace - // — that scenario is owned by EXP0021 (a higher-severity Warning with its own codefix + // — that scenario is owned by EXP0026 (a higher-severity Warning with its own codefix // for adding the missing `using`). Reporting both would just be duplicate noise. if (expressiveQueryableOpenGeneric is not null && FindExpressiveSiblingNamespace(context.SemanticModel.Compilation, calledName, expressiveQueryableOpenGeneric) is not null) diff --git a/src/ExpressiveSharp.CodeFixers/MissingExpressiveAnalyzer.cs b/src/ExpressiveSharp.CodeFixers/MissingExpressiveAnalyzer.cs index d784176..1be81fe 100644 --- a/src/ExpressiveSharp.CodeFixers/MissingExpressiveAnalyzer.cs +++ b/src/ExpressiveSharp.CodeFixers/MissingExpressiveAnalyzer.cs @@ -9,7 +9,7 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Reports EXP0013 when a member referenced inside an [Expressive] body, an +/// Reports EXP0025 when a member referenced inside an [Expressive] body, an /// ExpressionPolyfill.Create() lambda, or an IExpressiveQueryable /// LINQ lambda has an expandable body but is not marked [Expressive]. /// @@ -17,7 +17,7 @@ namespace ExpressiveSharp.CodeFixers; public sealed class MissingExpressiveAnalyzer : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor MemberCouldBeExpressive = new( - id: "EXP0013", + id: "EXP0025", title: "Referenced member could benefit from [Expressive]", messageFormat: "Member '{0}' is referenced in an [Expressive] expression but is not marked [Expressive]. Adding [Expressive] would allow its body to be inlined into the expression tree.", category: "Design", @@ -157,7 +157,7 @@ identifier.Parent is not MemberAccessExpressionSyntax && } // Generator already expands enum/Nullable receivers via TryEmitEnumMethodExpansion, - // so EXP0013 would be a false positive. + // so EXP0025 would be a false positive. private static bool HasEnumReceiver( IMethodSymbol method, InvocationExpressionSyntax invocation, diff --git a/src/ExpressiveSharp.CodeFixers/MissingExpressiveImportAnalyzer.cs b/src/ExpressiveSharp.CodeFixers/MissingExpressiveImportAnalyzer.cs index b9cc2aa..8ba0fc5 100644 --- a/src/ExpressiveSharp.CodeFixers/MissingExpressiveImportAnalyzer.cs +++ b/src/ExpressiveSharp.CodeFixers/MissingExpressiveImportAnalyzer.cs @@ -8,17 +8,17 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Reports EXP0021 when a LINQ method on an IExpressiveQueryable<T> receiver resolves to +/// Reports EXP0026 when a LINQ method on an IExpressiveQueryable<T> receiver resolves to /// System.Linq.Queryable instead of the ExpressiveSharp delegate-based overload — typically /// because using ExpressiveSharp; is missing. -/// Reports EXP0022 when no ExpressiveSharp stub exists for the called method, meaning the +/// Reports EXP0027 when no ExpressiveSharp stub exists for the called method, meaning the /// IExpressiveQueryable chain will be broken. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class MissingExpressiveImportAnalyzer : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor StubNotResolved = new( - id: "EXP0021", + id: "EXP0026", title: "IExpressiveQueryable LINQ method resolves to Queryable", messageFormat: "LINQ method '{0}' on IExpressiveQueryable resolves to System.Linq.Queryable instead of the ExpressiveSharp overload. Add 'using ExpressiveSharp;' to enable expression tree rewriting and maintain the IExpressiveQueryable chain.", category: "Usage", @@ -26,7 +26,7 @@ public sealed class MissingExpressiveImportAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true); public static readonly DiagnosticDescriptor NoStubExists = new( - id: "EXP0022", + id: "EXP0027", title: "No IExpressiveQueryable overload for Queryable method", messageFormat: "Method '{0}' from System.Linq.Queryable has no IExpressiveQueryable overload. The result will be IQueryable, breaking the IExpressiveQueryable chain.", category: "Usage", diff --git a/src/ExpressiveSharp.CodeFixers/PlainQueryableMissingAsExpressiveAnalyzer.cs b/src/ExpressiveSharp.CodeFixers/PlainQueryableMissingAsExpressiveAnalyzer.cs index d5cddc3..5d58c64 100644 --- a/src/ExpressiveSharp.CodeFixers/PlainQueryableMissingAsExpressiveAnalyzer.cs +++ b/src/ExpressiveSharp.CodeFixers/PlainQueryableMissingAsExpressiveAnalyzer.cs @@ -10,7 +10,7 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Reports EXP0027 when a LINQ method on a plain receiver +/// Reports EXP0028 when a LINQ method on a plain receiver /// (not IExpressiveQueryable<T>) is invoked with a lambda whose body references an /// [Expressive] member. Without .AsExpressive(), the body of the referenced member /// is not expanded into the query tree and the provider may silently fall back to client-side @@ -20,7 +20,7 @@ namespace ExpressiveSharp.CodeFixers; public sealed class PlainQueryableMissingAsExpressiveAnalyzer : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor PlainQueryableMissingAsExpressive = new( - id: "EXP0027", + id: "EXP0028", title: "Plain IQueryable chain references an [Expressive] member without .AsExpressive()", messageFormat: "LINQ method '{0}' on a plain IQueryable references the [Expressive] member '{1}'. Without .AsExpressive(), the member's body will not be inlined into the expression tree; the provider may evaluate the call in memory or fail to translate it. Wrap the source with .AsExpressive().", category: "Usage", @@ -62,7 +62,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) if (!ImplementsIQueryable(receiverType)) return; - // If the chain is already an IExpressiveQueryable, the existing EXP0013 / EXP0021 + // If the chain is already an IExpressiveQueryable, the existing EXP0025 / EXP0026 // diagnostics cover it. if (ExpressiveSymbolHelpers.IsOrImplementsExpressiveQueryable(receiverType)) return; diff --git a/src/ExpressiveSharp.CodeFixers/WrapInAsExpressiveCodeFixProvider.cs b/src/ExpressiveSharp.CodeFixers/WrapInAsExpressiveCodeFixProvider.cs index bad8a59..fee2eed 100644 --- a/src/ExpressiveSharp.CodeFixers/WrapInAsExpressiveCodeFixProvider.cs +++ b/src/ExpressiveSharp.CodeFixers/WrapInAsExpressiveCodeFixProvider.cs @@ -12,7 +12,7 @@ namespace ExpressiveSharp.CodeFixers; /// -/// Provides a code fix for EXP0027 that wraps the chain root with .AsExpressive() +/// Provides a code fix for EXP0028 that wraps the chain root with .AsExpressive() /// (and adds using ExpressiveSharp; if needed) so that subsequent LINQ methods /// flow through the ExpressiveSharp delegate-based overloads. /// @@ -21,7 +21,7 @@ namespace ExpressiveSharp.CodeFixers; public sealed class WrapInAsExpressiveCodeFixProvider : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds => - ImmutableArray.Create("EXP0027"); + ImmutableArray.Create("EXP0028"); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -44,7 +44,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) CodeAction.Create( title: "Wrap source with .AsExpressive()", createChangedDocument: ct => WrapWithAsExpressiveAsync(context.Document, invocation, ct), - equivalenceKey: "EXP0027_WrapAsExpressive"), + equivalenceKey: "EXP0028_WrapAsExpressive"), diagnostic); } } diff --git a/src/ExpressiveSharp.EntityFrameworkCore.CodeFixers/WindowFunctionLiteralArgsAnalyzer.cs b/src/ExpressiveSharp.EntityFrameworkCore.CodeFixers/WindowFunctionLiteralArgsAnalyzer.cs index 8967605..ca8b5cc 100644 --- a/src/ExpressiveSharp.EntityFrameworkCore.CodeFixers/WindowFunctionLiteralArgsAnalyzer.cs +++ b/src/ExpressiveSharp.EntityFrameworkCore.CodeFixers/WindowFunctionLiteralArgsAnalyzer.cs @@ -13,7 +13,7 @@ public sealed class WindowFunctionLiteralArgsAnalyzer : DiagnosticAnalyzer "ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.WindowFunctions.WindowFunction"; public static readonly DiagnosticDescriptor NtileRequiresPositiveBuckets = new( - id: "EXP0036", + id: "EXP0030", title: "WindowFunction.Ntile requires a positive bucket count", messageFormat: "WindowFunction.Ntile requires a positive bucket count; literal value {0} produces invalid SQL", category: "Usage", @@ -22,7 +22,7 @@ public sealed class WindowFunctionLiteralArgsAnalyzer : DiagnosticAnalyzer description: "NTILE(n) divides ordered rows into n buckets. SQL requires n >= 1; non-positive values raise a database error at execution time."); public static readonly DiagnosticDescriptor NavigationOffsetMustBeNonNegative = new( - id: "EXP0037", + id: "EXP0031", title: "WindowFunction.Lag/Lead offset must be non-negative", messageFormat: "WindowFunction.{0} offset must be non-negative; literal value {1} is rejected during EF translation", category: "Usage", diff --git a/src/ExpressiveSharp.Generator/ExpressiveGenerator.cs b/src/ExpressiveSharp.Generator/ExpressiveGenerator.cs index a817b14..0000f00 100644 --- a/src/ExpressiveSharp.Generator/ExpressiveGenerator.cs +++ b/src/ExpressiveSharp.Generator/ExpressiveGenerator.cs @@ -229,7 +229,7 @@ private static void Execute( factoryCandidate.Identifier.Text)); } - // EXP0038: virtual/abstract/override members are expanded using the static (declared) + // EXP0024: virtual/abstract/override members are expanded using the static (declared) // type. Once the body is inlined into an expression tree (EF Core, MongoDB, ...), C# // virtual dispatch is lost, so an overridden body in a derived type is never used. if (memberSymbol.IsVirtual || memberSymbol.IsAbstract || memberSymbol.IsOverride) @@ -450,7 +450,7 @@ private static void ExecuteFor( descriptor.MemberName, descriptor.ParameterTypeNames); var generatedFileName = $"{generatedClassName}.{methodSuffix}.g.cs"; - // Skip duplicate emissions — EXP0020 is reported via the registry duplicate check + // Skip duplicate emissions — EXP0017 is reported via the registry duplicate check if (emittedFileNames is not null && !emittedFileNames.Add(generatedFileName)) return; @@ -701,7 +701,7 @@ private static Compilation AugmentCompilation( // Pair every (decl, originalCompilation) with the augmented compilation. Validation // and ChooseBackingNames must use the original compilation (augmentation would otherwise - // false-positive EXP0031 against synthesized siblings); body-binding uses the augmented + // false-positive EXP0018 against synthesized siblings); body-binding uses the augmented // SemanticModel so cross-references between [ExpressiveProperty] stubs resolve. var compilationAndPairsWithBinding = compilationAndPairs.Combine(bindingCompilationProvider); diff --git a/src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs b/src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs index e1cab2b..06c8128 100644 --- a/src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs +++ b/src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs @@ -92,9 +92,6 @@ static internal class Diagnostics DiagnosticSeverity.Warning, isEnabledByDefault: true); - // EXP0013 (MemberCouldBeExpressive) is emitted by MissingExpressiveAnalyzer - // in ExpressiveSharp.CodeFixers so VS can pair it with the code fix provider. - public readonly static DiagnosticDescriptor FactoryMethodShouldBeConstructor = new DiagnosticDescriptor( id: "EXP0012", title: "[Expressive] factory method can be converted to a constructor", @@ -104,7 +101,7 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressiveForTargetTypeNotFound = new DiagnosticDescriptor( - id: "EXP0014", + id: "EXP0013", title: "[ExpressiveFor] target type not found", messageFormat: "[ExpressiveFor] target type '{0}' could not be resolved", category: "Design", @@ -112,35 +109,26 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressiveForMemberNotFound = new DiagnosticDescriptor( - id: "EXP0015", + id: "EXP0014", title: "[ExpressiveFor] target member not found", messageFormat: "No member '{0}' found on type '{1}' matching the stub's parameter signature", category: "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - // EXP0016 (ExpressiveForStubMustBeStatic) is retired — instance stubs are now permitted - // on the target type itself. Constructor stubs remain static-only; signature mismatches fall - // back to EXP0015 (ExpressiveForMemberNotFound). + // Note: instance stubs are permitted on the target type itself; constructor stubs remain + // static-only. Signature mismatches fall back to EXP0014 (ExpressiveForMemberNotFound). public readonly static DiagnosticDescriptor ExpressiveForReturnTypeMismatch = new DiagnosticDescriptor( - id: "EXP0017", + id: "EXP0015", title: "[ExpressiveFor] return type mismatch", messageFormat: "[ExpressiveFor] return type mismatch for '{0}': target returns '{1}' but stub returns '{2}'", category: "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - public readonly static DiagnosticDescriptor IgnoredOperation = new DiagnosticDescriptor( - id: "EXP0018", - title: "Unsupported operation ignored", - messageFormat: "Expression contains an unsupported operation ({0}). The operation will be ignored and the surrounding expression emitted without it.", - category: "Design", - DiagnosticSeverity.Warning, - isEnabledByDefault: true); - public readonly static DiagnosticDescriptor ExpressiveForConflictsWithExpressive = new DiagnosticDescriptor( - id: "EXP0019", + id: "EXP0016", title: "[ExpressiveFor] conflicts with [Expressive]", messageFormat: "Target member '{0}' on type '{1}' already has [Expressive]; remove [ExpressiveFor] or [Expressive]", category: "Design", @@ -148,19 +136,15 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressiveForDuplicateMapping = new DiagnosticDescriptor( - id: "EXP0020", + id: "EXP0017", title: "Duplicate [ExpressiveFor] mapping", messageFormat: "Duplicate [ExpressiveFor] mapping for member '{0}' on type '{1}'; only one stub per target member is allowed", category: "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - // EXP0021–EXP0030 (Projectable diagnostics) were retired when the - // [Expressive(Projectable = true)] feature was superseded by [ExpressiveProperty]. - // The codes are not reused. - public readonly static DiagnosticDescriptor ExpressivePropertyTargetExists = new DiagnosticDescriptor( - id: "EXP0031", + id: "EXP0018", title: "[ExpressiveProperty] target name is already defined", messageFormat: "[ExpressiveProperty] target name '{0}' is already defined on '{1}' — rename the stub, or use [ExpressiveFor(nameof({0}))] to map onto the existing member instead", category: "Design", @@ -168,7 +152,7 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressivePropertyRequiresPartial = new DiagnosticDescriptor( - id: "EXP0032", + id: "EXP0019", title: "[ExpressiveProperty] requires a partial containing type", messageFormat: "[ExpressiveProperty] requires the containing type '{0}' to be declared 'partial' (applies to class, struct, and record)", category: "Design", @@ -176,7 +160,7 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressivePropertyRequiresExpressionBody = new DiagnosticDescriptor( - id: "EXP0033", + id: "EXP0020", title: "[ExpressiveProperty] requires an expression-bodied property stub", messageFormat: "[ExpressiveProperty] must be placed on a property with an expression body '=> expr' — accessor-list forms and method stubs are not supported", category: "Design", @@ -184,7 +168,7 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressivePropertyInstanceOnly = new DiagnosticDescriptor( - id: "EXP0034", + id: "EXP0021", title: "[ExpressiveProperty] requires an instance stub", messageFormat: "[ExpressiveProperty] is not supported on static stubs — stub '{0}' must be declared as an instance member", category: "Design", @@ -192,21 +176,35 @@ static internal class Diagnostics isEnabledByDefault: true); public readonly static DiagnosticDescriptor ExpressivePropertyShadowsInherited = new DiagnosticDescriptor( - id: "EXP0035", + id: "EXP0022", title: "[ExpressiveProperty] target shadows inherited member", messageFormat: "[ExpressiveProperty] target name '{0}' shadows an inherited member on '{1}' — rename the target to avoid silent hiding, or drop [ExpressiveProperty] and use [Expressive] on an override", category: "Design", DiagnosticSeverity.Error, isEnabledByDefault: true); - // EXP0036/EXP0037 live in WindowFunctionLiteralArgsAnalyzer (EntityFrameworkCore.CodeFixers). + public readonly static DiagnosticDescriptor IgnoredOperation = new DiagnosticDescriptor( + id: "EXP0023", + title: "Unsupported operation ignored", + messageFormat: "Expression contains an unsupported operation ({0}). The operation will be ignored and the surrounding expression emitted without it.", + category: "Design", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); public readonly static DiagnosticDescriptor VirtualMemberDispatchedStatically = new DiagnosticDescriptor( - id: "EXP0038", + id: "EXP0024", title: "[Expressive] member is virtual and will not dispatch polymorphically", messageFormat: "[Expressive] member '{0}' is virtual, abstract, or an override. When it is expanded into an expression tree (e.g. for EF Core or MongoDB), the call is resolved using the static (declared) type, so an overridden body in a derived type is never used. Test the runtime type explicitly (e.g. 'x switch {{ Derived d => d.Member, _ => x.Member }}'), or move the logic into a non-virtual [Expressive] static/extension method.", category: "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Expression-tree expansion happens at compile time and only sees the static (declared) type of the receiver, so it cannot honor C# virtual dispatch. Declaring an [Expressive] member virtual/abstract/override therefore silently expands the base body for query providers. This differs from compiling the expression to a delegate and invoking it in memory, where the CLR dispatches on the runtime type."); + + // Diagnostics defined outside this file: + // EXP0025 MemberCouldBeExpressive, EXP0026 StubNotResolved, EXP0027 NoStubExists, + // EXP0028 PlainQueryableMissingAsExpressive, EXP0029 ExpressiveQueryableDropout + // — analyzers in ExpressiveSharp.CodeFixers (paired with their code fix providers). + // EXP0030 NtileRequiresPositiveBuckets, EXP0031 NavigationOffsetMustBeNonNegative + // — WindowFunctionLiteralArgsAnalyzer in ExpressiveSharp.EntityFrameworkCore.CodeFixers. + // EXP1001–EXP1003 — Projectables migration (MigrationAnalyzer, same assembly). } diff --git a/src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs b/src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs index 436ec3d..812bb68 100644 --- a/src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs +++ b/src/ExpressiveSharp.Generator/Interpretation/ExpressivePropertyInterpreter.cs @@ -19,7 +19,7 @@ static internal class ExpressivePropertyInterpreter SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - // semanticModel must be from the *original* (non-augmented) compilation so that the EXP0031 + // semanticModel must be from the *original* (non-augmented) compilation so that the EXP0018 // conflict check and ChooseBackingNames don't observe siblings synthesized by this pipeline. // bodyBindingSemanticModel may come from a compilation augmented with synthesized partials so // that one [ExpressiveProperty] body can reference another's synthesized target. Pass null to @@ -73,7 +73,7 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G return null; } - // Declared members first (EXP0031) because that's the more common mistake and deserves the + // Declared members first (EXP0018) because that's the more common mistake and deserves the // dedicated "use [ExpressiveFor] instead" steering. if (containingType.GetMembers(targetName!).Any(m => m is IPropertySymbol or IMethodSymbol or IFieldSymbol or IEventSymbol)) diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/AddExpressiveCodeFixProviderTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/AddExpressiveCodeFixProviderTests.cs index b7fb56f..3dc1383 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/AddExpressiveCodeFixProviderTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/AddExpressiveCodeFixProviderTests.cs @@ -177,10 +177,10 @@ private async Task ApplyCodeFixAsync( System.Collections.Immutable.ImmutableArray.Create(analyzer)); var analyzerDiagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(CancellationToken.None); - var diagnostic = analyzerDiagnostics.FirstOrDefault(d => d.Id == "EXP0013"); - Assert.IsNotNull(diagnostic, "Expected EXP0013 diagnostic to be emitted"); + var diagnostic = analyzerDiagnostics.FirstOrDefault(d => d.Id == "EXP0025"); + Assert.IsNotNull(diagnostic, "Expected EXP0025 diagnostic to be emitted"); Assert.IsTrue(diagnostic.AdditionalLocations.Count > 0, - "Expected additional location (declaration) on EXP0013"); + "Expected additional location (declaration) on EXP0025"); var usageTree = diagnostic.Location.SourceTree; Assert.IsNotNull(usageTree, "Diagnostic should have a source tree"); diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/ExpressiveQueryableDropoutAnalyzerTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/ExpressiveQueryableDropoutAnalyzerTests.cs index cf077d0..4d22381 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/ExpressiveQueryableDropoutAnalyzerTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/ExpressiveQueryableDropoutAnalyzerTests.cs @@ -62,7 +62,7 @@ public static System.Linq.IQueryable Sanitize(this System.Linq.IQueryable< """; [TestMethod] - public async Task ExpressiveReceiver_PlainHelper_ReportsEXP0036() + public async Task ExpressiveReceiver_PlainHelper_ReportsEXP0029() { const string source = """ using System.Linq; @@ -84,12 +84,12 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0036"), - "Expected EXP0036 when a plain-IQueryable helper is called on an expressive receiver"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0029"), + "Expected EXP0029 when a plain-IQueryable helper is called on an expressive receiver"); } [TestMethod] - public async Task ExpressiveReceiver_AsQueryable_NoEXP0036() + public async Task ExpressiveReceiver_AsQueryable_NoEXP0029() { // .AsQueryable() is a sanctioned explicit downcast — we treat it as user intent. const string source = """ @@ -111,8 +111,8 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "EXP0036 should not fire on an explicit .AsQueryable() — it is the sanctioned downcast"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "EXP0029 should not fire on an explicit .AsQueryable() — it is the sanctioned downcast"); } [TestMethod] @@ -146,13 +146,13 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - var hits = diagnostics.Count(d => d.Id == "EXP0036"); + var hits = diagnostics.Count(d => d.Id == "EXP0029"); Assert.AreEqual(1, hits, - "EXP0036 should fire once at the dropout point, not at downstream Include/Where/etc."); + "EXP0029 should fire once at the dropout point, not at downstream Include/Where/etc."); } [TestMethod] - public async Task ExpressiveReceiver_ExpressiveShadow_NoEXP0036() + public async Task ExpressiveReceiver_ExpressiveShadow_NoEXP0029() { const string source = """ using System.Linq; @@ -174,12 +174,12 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "EXP0036 should not fire when the called method preserves IExpressiveQueryable"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "EXP0029 should not fire when the called method preserves IExpressiveQueryable"); } [TestMethod] - public async Task PlainReceiver_NoEXP0036() + public async Task PlainReceiver_NoEXP0029() { const string source = """ using System.Linq; @@ -200,12 +200,12 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "EXP0036 should not fire when the chain was never expressive to begin with"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "EXP0029 should not fire when the chain was never expressive to begin with"); } [TestMethod] - public async Task ExpressiveReceiver_NotExpressiveMethod_NoEXP0036() + public async Task ExpressiveReceiver_NotExpressiveMethod_NoEXP0029() { const string source = """ using System.Linq; @@ -227,12 +227,12 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "[NotExpressive] on the offending method should suppress EXP0036"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "[NotExpressive] on the offending method should suppress EXP0029"); } [TestMethod] - public async Task ExpressiveReceiver_TerminatingCall_NoEXP0036() + public async Task ExpressiveReceiver_TerminatingCall_NoEXP0029() { const string source = """ using System.Collections.Generic; @@ -254,12 +254,12 @@ void M(IQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "EXP0036 should not fire on terminating calls whose result is not IQueryable"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "EXP0029 should not fire on terminating calls whose result is not IQueryable"); } [TestMethod] - public async Task ExpressiveReceiver_BuiltInWhereWithInlineLambda_NoEXP0036() + public async Task ExpressiveReceiver_BuiltInWhereWithInlineLambda_NoEXP0029() { // Queryable.Where on an IExpressiveQueryable receiver is rewritten by the // polyfill interceptor into the IExpressiveQueryable.Where stub at compile time, @@ -284,16 +284,16 @@ void M(IExpressiveQueryable orders) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "EXP0036 should not fire on built-in LINQ calls whose IExpressiveQueryable sibling stub exists."); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "EXP0029 should not fire on built-in LINQ calls whose IExpressiveQueryable sibling stub exists."); } [TestMethod] - public async Task ExpressiveDbSet_BuiltInWhereWithInlineLambda_OnlyEXP0021Fires_NotEXP0036() + public async Task ExpressiveDbSet_BuiltInWhereWithInlineLambda_OnlyEXP0026Fires_NotEXP0029() { // The user's StoryGrain shape: ExpressiveDbSet-style receiver, no - // `using ExpressiveSharp;`. EXP0021 owns this scenario (Warning + codefix); - // EXP0036 stays silent here so the user gets one actionable diagnostic + // `using ExpressiveSharp;`. EXP0026 owns this scenario (Warning + codefix); + // EXP0029 stays silent here so the user gets one actionable diagnostic // instead of two overlapping ones. const string source = """ using System.Linq; @@ -326,10 +326,10 @@ void M(StubExpressiveDbSet orders) new ExpressiveQueryableDropoutAnalyzer(), new MissingExpressiveImportAnalyzer()); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0021"), - "EXP0021 should own the missing-using scenario."); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036"), - "EXP0036 should suppress itself when EXP0021 covers the same dropout cause."); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0026"), + "EXP0026 should own the missing-using scenario."); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0029"), + "EXP0029 should suppress itself when EXP0026 covers the same dropout cause."); } private async Task> GetDiagnosticsAsync(string source) diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/MissingExpressiveImportAnalyzerTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/MissingExpressiveImportAnalyzerTests.cs index c06cb97..08966a8 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/MissingExpressiveImportAnalyzerTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/MissingExpressiveImportAnalyzerTests.cs @@ -16,7 +16,7 @@ namespace ExpressiveSharp.Generator.Tests.CodeFixers; public sealed class MissingExpressiveImportAnalyzerTests : GeneratorTestBase { [TestMethod] - public async Task Where_WithoutUsing_ReportsEXP0021() + public async Task Where_WithoutUsing_ReportsEXP0026() { const string source = """ using System; @@ -35,12 +35,12 @@ void M(ExpressiveSharp.IExpressiveQueryable q) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0021"), - "Expected EXP0021 for Where on IExpressiveQueryable without using"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0026"), + "Expected EXP0026 for Where on IExpressiveQueryable without using"); } [TestMethod] - public async Task Take_WithoutUsing_ReportsEXP0021() + public async Task Take_WithoutUsing_ReportsEXP0026() { const string source = """ using System; @@ -59,8 +59,8 @@ void M(ExpressiveSharp.IExpressiveQueryable q) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0021"), - "Expected EXP0021 for Take on IExpressiveQueryable without using (chain break)"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0026"), + "Expected EXP0026 for Take on IExpressiveQueryable without using (chain break)"); } [TestMethod] @@ -84,8 +84,8 @@ void M(IExpressiveQueryable q) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0021"), - "Should not report EXP0021 when using ExpressiveSharp is present"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0026"), + "Should not report EXP0026 when using ExpressiveSharp is present"); } [TestMethod] @@ -108,12 +108,12 @@ void M(IQueryable q) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0021" || d.Id == "EXP0022"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0026" || d.Id == "EXP0027"), "Should not report any diagnostic for plain IQueryable"); } [TestMethod] - public async Task Contains_WithoutStub_ReportsEXP0022() + public async Task Contains_WithoutStub_ReportsEXP0027() { const string source = """ using System; @@ -132,10 +132,10 @@ void M(ExpressiveSharp.IExpressiveQueryable q) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0022"), - "Expected EXP0022 for Contains (no stub exists)"); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0021"), - "Should not report EXP0021 when no stub exists"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0027"), + "Expected EXP0027 for Contains (no stub exists)"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0026"), + "Should not report EXP0026 when no stub exists"); } [TestMethod] @@ -158,7 +158,7 @@ void M(IQueryable q) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0021" || d.Id == "EXP0022"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0026" || d.Id == "EXP0027"), "Should not report any diagnostic for plain IQueryable"); } diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/NotExpressiveOptOutTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/NotExpressiveOptOutTests.cs index 7f73d85..81f90c5 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/NotExpressiveOptOutTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/NotExpressiveOptOutTests.cs @@ -16,7 +16,7 @@ namespace ExpressiveSharp.Generator.Tests.CodeFixers; public sealed class NotExpressiveOptOutTests : GeneratorTestBase { [TestMethod] - public async Task NotExpressive_OnReferencedMember_SuppressesEXP0013() + public async Task NotExpressive_OnReferencedMember_SuppressesEXP0025() { const string source = """ using ExpressiveSharp; @@ -35,12 +35,12 @@ class C var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), - "[NotExpressive] should suppress EXP0013 on the referenced member"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), + "[NotExpressive] should suppress EXP0025 on the referenced member"); } [TestMethod] - public async Task NoOptOut_StillReportsEXP0013() + public async Task NoOptOut_StillReportsEXP0025() { const string source = """ using ExpressiveSharp; @@ -58,8 +58,8 @@ class C var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "EXP0013 should still fire when [NotExpressive] is absent"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "EXP0025 should still fire when [NotExpressive] is absent"); } private async Task> GetDiagnosticsAsync(string source) diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/PlainQueryableMissingAsExpressiveAnalyzerTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/PlainQueryableMissingAsExpressiveAnalyzerTests.cs index 1da314c..61b5f2f 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/PlainQueryableMissingAsExpressiveAnalyzerTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/PlainQueryableMissingAsExpressiveAnalyzerTests.cs @@ -16,7 +16,7 @@ namespace ExpressiveSharp.Generator.Tests.CodeFixers; public sealed class PlainQueryableMissingAsExpressiveAnalyzerTests : GeneratorTestBase { [TestMethod] - public async Task PlainQueryable_WithExpressiveProperty_ReportsEXP0027() + public async Task PlainQueryable_WithExpressiveProperty_ReportsEXP0028() { const string source = """ using System.Linq; @@ -43,12 +43,12 @@ void M(IQueryable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0027"), - "Expected EXP0027 when plain IQueryable lambda references an [Expressive] member"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0028"), + "Expected EXP0028 when plain IQueryable lambda references an [Expressive] member"); } [TestMethod] - public async Task ExpressiveQueryable_NoEXP0027() + public async Task ExpressiveQueryable_NoEXP0028() { const string source = """ using System.Linq; @@ -75,12 +75,12 @@ void M(IExpressiveQueryable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0027"), - "EXP0027 should not fire when the receiver is already IExpressiveQueryable"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0028"), + "EXP0028 should not fire when the receiver is already IExpressiveQueryable"); } [TestMethod] - public async Task PlainQueryable_NonExpressiveMember_NoEXP0027() + public async Task PlainQueryable_NonExpressiveMember_NoEXP0028() { const string source = """ using System.Linq; @@ -103,12 +103,12 @@ void M(IQueryable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0027"), - "EXP0027 should not fire when no [Expressive] member is referenced"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0028"), + "EXP0028 should not fire when no [Expressive] member is referenced"); } [TestMethod] - public async Task PlainEnumerable_NoEXP0027() + public async Task PlainEnumerable_NoEXP0028() { const string source = """ using System.Collections.Generic; @@ -136,12 +136,12 @@ void M(IEnumerable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0027"), - "EXP0027 should not fire on IEnumerable (LINQ-to-Objects) chains"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0028"), + "EXP0028 should not fire on IEnumerable (LINQ-to-Objects) chains"); } [TestMethod] - public async Task PlainQueryable_WithNotExpressiveMember_NoEXP0027() + public async Task PlainQueryable_WithNotExpressiveMember_NoEXP0028() { const string source = """ using System.Linq; @@ -168,12 +168,12 @@ void M(IQueryable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0027"), - "[NotExpressive] should suppress EXP0027"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0028"), + "[NotExpressive] should suppress EXP0028"); } [TestMethod] - public async Task PlainQueryable_AfterAsExpressive_NoEXP0027() + public async Task PlainQueryable_AfterAsExpressive_NoEXP0028() { const string source = """ using System.Linq; @@ -200,12 +200,12 @@ void M(IQueryable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0027"), - "EXP0027 should not fire when the chain is already wrapped with .AsExpressive()"); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0028"), + "EXP0028 should not fire when the chain is already wrapped with .AsExpressive()"); } [TestMethod] - public async Task PlainQueryable_ExpressiveMethodInLambda_ReportsEXP0027() + public async Task PlainQueryable_ExpressiveMethodInLambda_ReportsEXP0028() { const string source = """ using System.Linq; @@ -232,8 +232,8 @@ void M(IQueryable users) var diagnostics = await GetDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0027"), - "Expected EXP0027 when plain IQueryable lambda references an [Expressive] method"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0028"), + "Expected EXP0028 when plain IQueryable lambda references an [Expressive] method"); } // ── Helpers ────────────────────────────────────────────────────────────── diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WindowFunctionLiteralArgsAnalyzerTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WindowFunctionLiteralArgsAnalyzerTests.cs index a670ce0..a5c2f2a 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WindowFunctionLiteralArgsAnalyzerTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WindowFunctionLiteralArgsAnalyzerTests.cs @@ -34,7 +34,7 @@ public static class WindowFunction """; [TestMethod] - public async Task NtileWithZeroBuckets_ReportsEXP0036() + public async Task NtileWithZeroBuckets_ReportsEXP0030() { const string source = """ using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.WindowFunctions; @@ -45,12 +45,12 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0036"), - "Expected EXP0036 for Ntile(0)"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0030"), + "Expected EXP0030 for Ntile(0)"); } [TestMethod] - public async Task NtileWithNegativeBuckets_ReportsEXP0036() + public async Task NtileWithNegativeBuckets_ReportsEXP0030() { const string source = """ using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.WindowFunctions; @@ -61,7 +61,7 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0036")); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0030")); } [TestMethod] @@ -76,7 +76,7 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036")); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0030")); } [TestMethod] @@ -91,11 +91,11 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0036")); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0030")); } [TestMethod] - public async Task LagWithNegativeOffset_ReportsEXP0037() + public async Task LagWithNegativeOffset_ReportsEXP0031() { const string source = """ using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.WindowFunctions; @@ -106,11 +106,11 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0037")); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0031")); } [TestMethod] - public async Task LeadWithNegativeOffset_ReportsEXP0037() + public async Task LeadWithNegativeOffset_ReportsEXP0031() { const string source = """ using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.WindowFunctions; @@ -121,7 +121,7 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0037")); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0031")); } [TestMethod] @@ -136,7 +136,7 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0037")); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0031")); } [TestMethod] @@ -151,7 +151,7 @@ class C """; var diagnostics = await GetWindowDiagnosticsAsync(source); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0037")); + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0031")); } private async Task> GetWindowDiagnosticsAsync(string source) diff --git a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WrapInAsExpressiveCodeFixProviderTests.cs b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WrapInAsExpressiveCodeFixProviderTests.cs index d884af7..7759b11 100644 --- a/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WrapInAsExpressiveCodeFixProviderTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/CodeFixers/WrapInAsExpressiveCodeFixProviderTests.cs @@ -143,8 +143,8 @@ private async Task ApplyCodeFixAsync(string source) ImmutableArray.Create(analyzer)); var analyzerDiagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(CancellationToken.None); - var diagnostic = analyzerDiagnostics.FirstOrDefault(d => d.Id == "EXP0027"); - Assert.IsNotNull(diagnostic, "Expected EXP0027 diagnostic to be emitted"); + var diagnostic = analyzerDiagnostics.FirstOrDefault(d => d.Id == "EXP0028"); + Assert.IsNotNull(diagnostic, "Expected EXP0028 diagnostic to be emitted"); var docInSolution = project.Solution.GetDocument(diagnostic.Location.SourceTree) ?? throw new System.Exception("Failed to locate document for diagnostic"); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/DiagnosticTests.cs b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/DiagnosticTests.cs index a7532ad..be26d5b 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/DiagnosticTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/DiagnosticTests.cs @@ -97,7 +97,7 @@ class C { } [TestMethod] - public void StringInterpolation_AlignmentSpecifier_ReportsEXP0018() + public void StringInterpolation_AlignmentSpecifier_ReportsEXP0023() { var compilation = CreateCompilation( """ @@ -112,8 +112,8 @@ class C { """); var result = RunExpressiveGenerator(compilation); - Assert.IsTrue(result.Diagnostics.Any(d => d.Id == "EXP0018"), - "Expected EXP0018 for alignment specifier in string interpolation"); + Assert.IsTrue(result.Diagnostics.Any(d => d.Id == "EXP0023"), + "Expected EXP0023 for alignment specifier in string interpolation"); Assert.IsTrue(result.GeneratedTrees.Length > 0, "Generator should still produce output (interpolation without alignment)"); } @@ -190,7 +190,7 @@ public static PersonDto Create(string name, int age) => } [TestMethod] - public void ExpressiveFor_NonExistentTargetType_ReportsEXP0014() + public void ExpressiveFor_NonExistentTargetType_ReportsEXP0013() { var compilation = CreateCompilation( """ @@ -205,12 +205,12 @@ static class Mappings { """); var result = RunExpressiveGenerator(compilation); - Assert.IsTrue(result.Diagnostics.Any(d => d.Id == "EXP0014"), - "Expected EXP0014 for unresolvable target type in [ExpressiveFor]"); + Assert.IsTrue(result.Diagnostics.Any(d => d.Id == "EXP0013"), + "Expected EXP0013 for unresolvable target type in [ExpressiveFor]"); } [TestMethod] - public void VirtualMethod_ReportsEXP0038() + public void VirtualMethod_ReportsEXP0024() { var compilation = CreateCompilation( """ @@ -225,15 +225,15 @@ class Animal { """); var result = RunExpressiveGenerator(compilation); - var diag = result.Diagnostics.FirstOrDefault(d => d.Id == "EXP0038"); - Assert.IsNotNull(diag, "Expected EXP0038 for a virtual [Expressive] member"); + var diag = result.Diagnostics.FirstOrDefault(d => d.Id == "EXP0024"); + Assert.IsNotNull(diag, "Expected EXP0024 for a virtual [Expressive] member"); Assert.AreEqual(DiagnosticSeverity.Warning, diag.Severity); Assert.IsTrue(result.GeneratedTrees.Length > 0, - "Generator should still produce output alongside the EXP0038 warning"); + "Generator should still produce output alongside the EXP0024 warning"); } [TestMethod] - public void VirtualAndOverrideProperties_BothReportEXP0038() + public void VirtualAndOverrideProperties_BothReportEXP0024() { var compilation = CreateCompilation( """ @@ -253,12 +253,12 @@ class Dog : Animal { """); var result = RunExpressiveGenerator(compilation); - Assert.AreEqual(2, result.Diagnostics.Count(d => d.Id == "EXP0038"), - "Expected EXP0038 for both the virtual base property and its override"); + Assert.AreEqual(2, result.Diagnostics.Count(d => d.Id == "EXP0024"), + "Expected EXP0024 for both the virtual base property and its override"); } [TestMethod] - public void NonVirtualMember_DoesNotReportEXP0038() + public void NonVirtualMember_DoesNotReportEXP0024() { var compilation = CreateCompilation( """ @@ -273,8 +273,8 @@ class Animal { """); var result = RunExpressiveGenerator(compilation); - Assert.IsFalse(result.Diagnostics.Any(d => d.Id == "EXP0038"), - "A non-virtual [Expressive] member must not report EXP0038"); + Assert.IsFalse(result.Diagnostics.Any(d => d.Id == "EXP0024"), + "A non-virtual [Expressive] member must not report EXP0024"); } // NOTE: EXP0010 (InterceptorEmissionFailed) is intentionally not tested. diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressiveForTests.cs b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressiveForTests.cs index f83a066..5417fb0 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressiveForTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressiveForTests.cs @@ -172,7 +172,7 @@ static class Mappings { } [TestMethod] - public void MemberNotFound_EXP0015() + public void MemberNotFound_EXP0014() { var compilation = CreateCompilation( """ @@ -188,11 +188,11 @@ static class Mappings { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void InstanceStubOnUnrelatedType_Rejected_EXP0015() + public void InstanceStubOnUnrelatedType_Rejected_EXP0014() { // Instance stub targeting System.Math.Abs — stub's containing type is `Mappings`, // which does not match `System.Math`, so no member should be found. @@ -210,7 +210,7 @@ class Mappings { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] @@ -320,7 +320,7 @@ class MyType { } [TestMethod] - public void ReturnTypeMismatch_EXP0017() + public void ReturnTypeMismatch_EXP0015() { var compilation = CreateCompilation( """ @@ -336,11 +336,11 @@ static class Mappings { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0017", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); } [TestMethod] - public void ConflictWithExpressive_EXP0019() + public void ConflictWithExpressive_EXP0016() { var compilation = CreateCompilation( """ @@ -362,12 +362,12 @@ static class Mappings { """); var result = RunExpressiveGenerator(compilation); - var exp0019 = result.Diagnostics.Where(d => d.Id == "EXP0019").ToArray(); - Assert.AreEqual(1, exp0019.Length); + var exp0016 = result.Diagnostics.Where(d => d.Id == "EXP0016").ToArray(); + Assert.AreEqual(1, exp0016.Length); } [TestMethod] - public void DuplicateMapping_EXP0020() + public void DuplicateMapping_EXP0017() { var compilation = CreateCompilation( """ @@ -387,8 +387,8 @@ static class Mappings2 { """); var result = RunExpressiveGenerator(compilation); - var exp0020 = result.Diagnostics.Where(d => d.Id == "EXP0020").ToArray(); - Assert.AreEqual(2, exp0020.Length); + var exp0017 = result.Diagnostics.Where(d => d.Id == "EXP0017").ToArray(); + Assert.AreEqual(2, exp0017.Length); } // Each test below exercises one specific branch of @@ -426,7 +426,7 @@ static class Mappings { } [TestMethod] - public void InstanceStub_StaticPropertyTarget_Rejected_EXP0015() + public void InstanceStub_StaticPropertyTarget_Rejected_EXP0014() { // Static property + instance property stub → never matches (no way to supply receiver). var compilation = CreateCompilation( @@ -445,11 +445,11 @@ class MyType { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void PropertyStub_TargetingMethod_Rejected_EXP0015() + public void PropertyStub_TargetingMethod_Rejected_EXP0014() { // Property stubs can only target properties — even if a matching method exists. var compilation = CreateCompilation( @@ -469,11 +469,11 @@ class MyType { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void StaticStub_InstanceMethod_WrongReceiverType_Rejected_EXP0015() + public void StaticStub_InstanceMethod_WrongReceiverType_Rejected_EXP0014() { // Static stub over instance method, but the explicit receiver param is the wrong type. var compilation = CreateCompilation( @@ -495,11 +495,11 @@ static class Mappings { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void StaticStub_MethodTarget_WrongParamType_Rejected_EXP0015() + public void StaticStub_MethodTarget_WrongParamType_Rejected_EXP0014() { // Param count matches but a param type differs — hits the matcher's per-param loop. var compilation = CreateCompilation( @@ -516,11 +516,11 @@ static class Mappings { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void StaticStub_InstanceMethod_ParamCountMismatch_Rejected_EXP0015() + public void StaticStub_InstanceMethod_ParamCountMismatch_Rejected_EXP0014() { // Instance method has 1 param; static stub provides [receiver] only (missing the arg). var compilation = CreateCompilation( @@ -542,11 +542,11 @@ static class Mappings { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void PropertyStub_WithExplicitTargetType_WrongContainingType_Rejected_EXP0015() + public void PropertyStub_WithExplicitTargetType_WrongContainingType_Rejected_EXP0014() { // Property stub must be on the target type; [ExpressiveFor(typeof(Other))] on a stub // whose containing type is not Other cannot supply a receiver from `this`. @@ -568,11 +568,11 @@ class MyType { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void InstanceStub_InstanceMethod_ParamCountMismatch_Rejected_EXP0015() + public void InstanceStub_InstanceMethod_ParamCountMismatch_Rejected_EXP0014() { // Instance stub on target type, but arg count doesn't match the target method. var compilation = CreateCompilation( @@ -592,11 +592,11 @@ class MyType { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } [TestMethod] - public void SingleArgForm_UnknownMember_Rejected_EXP0015() + public void SingleArgForm_UnknownMember_Rejected_EXP0014() { // Single-arg form with a name that doesn't exist on the stub's containing type. var compilation = CreateCompilation( @@ -615,6 +615,6 @@ class MyType { var result = RunExpressiveGenerator(compilation); Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0015", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0014", result.Diagnostics[0].Id); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressivePropertyTests.cs b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressivePropertyTests.cs index afe8086..a055ddb 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressivePropertyTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressivePropertyTests.cs @@ -243,7 +243,7 @@ public partial class Inner { } [TestMethod] - public void TargetAlreadyExists_ReportsEXP0031() + public void TargetAlreadyExists_ReportsEXP0018() { var compilation = CreateCompilation( """ @@ -260,11 +260,11 @@ partial class Account { """); var result = RunExpressiveGenerator(compilation); - Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0031")); + Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0018")); } [TestMethod] - public void NonPartialContainer_ReportsEXP0032() + public void NonPartialContainer_ReportsEXP0019() { var compilation = CreateCompilation( """ @@ -281,11 +281,11 @@ class Account { """); var result = RunExpressiveGenerator(compilation); - Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0032")); + Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0019")); } [TestMethod] - public void AccessorListFormRejected_EXP0033() + public void AccessorListFormRejected_EXP0020() { // Accessor-list form (`{ get => expr; }`) is rejected in favor of top-level `=> expr`. var compilation = CreateCompilation( @@ -303,11 +303,11 @@ partial class Account { """); var result = RunExpressiveGenerator(compilation); - Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0033")); + Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0020")); } [TestMethod] - public void StaticStubRejected_EXP0034() + public void StaticStubRejected_EXP0021() { var compilation = CreateCompilation( """ @@ -324,7 +324,7 @@ partial class Account { """); var result = RunExpressiveGenerator(compilation); - Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0034")); + Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0021")); } [TestMethod] @@ -397,7 +397,7 @@ public partial class Person { } [TestMethod] - public void ShadowsInheritedMember_ReportsEXP0035() + public void ShadowsInheritedMember_ReportsEXP0022() { // Target name already exists on the base type — silently hiding it would be a footgun. var compilation = CreateCompilation( @@ -419,7 +419,7 @@ partial class Derived : Base { """); var result = RunExpressiveGenerator(compilation); - Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0035")); + Assert.AreEqual(1, result.Diagnostics.Count(d => d.Id == "EXP0022")); } [TestMethod] diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/InterfaceTests.cs b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/InterfaceTests.cs index 68433fc..e7d8870 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/InterfaceTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/InterfaceTests.cs @@ -55,11 +55,11 @@ public interface IDefaultBase : IBase var result = RunExpressiveGenerator(compilation); - // A default interface member is implicitly virtual, so EXP0038 fires: when expanded into + // A default interface member is implicitly virtual, so EXP0024 fires: when expanded into // an expression tree the call resolves against the static (interface) type, not a runtime // override. The generator still emits the expression for the declared body. Assert.AreEqual(1, result.Diagnostics.Length); - Assert.AreEqual("EXP0038", result.Diagnostics[0].Id); + Assert.AreEqual("EXP0024", result.Diagnostics[0].Id); Assert.AreEqual(1, result.GeneratedTrees.Length); return Verifier.Verify(result.GeneratedTrees[0].ToString()); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MissingExpressiveDiagnosticTests.cs b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MissingExpressiveDiagnosticTests.cs index 5f61435..1d0dc82 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MissingExpressiveDiagnosticTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MissingExpressiveDiagnosticTests.cs @@ -16,7 +16,7 @@ namespace ExpressiveSharp.Generator.Tests.ExpressiveGenerator; public class MissingExpressiveDiagnosticTests : GeneratorTestBase { [TestMethod] - public async Task MethodCall_ToSourceMethodWithExpressionBody_WarnsEXP0013() + public async Task MethodCall_ToSourceMethodWithExpressionBody_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -30,12 +30,12 @@ class C { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for method call to source method without [Expressive]"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for method call to source method without [Expressive]"); } [TestMethod] - public async Task PropertyAccess_ToSourcePropertyWithExpressionBody_WarnsEXP0013() + public async Task PropertyAccess_ToSourcePropertyWithExpressionBody_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -50,12 +50,12 @@ class C { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for property access to source property without [Expressive]"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for property access to source property without [Expressive]"); } [TestMethod] - public async Task PropertyAccess_ToSourcePropertyWithBlockGetter_WarnsEXP0013() + public async Task PropertyAccess_ToSourcePropertyWithBlockGetter_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -70,12 +70,12 @@ class C { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for property access to source property with block getter without [Expressive]"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for property access to source property with block getter without [Expressive]"); } [TestMethod] - public async Task MethodCall_ToBlockBodyMethod_WarnsEXP0013() + public async Task MethodCall_ToBlockBodyMethod_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -89,12 +89,12 @@ class C { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for method call to block-body method without [Expressive]"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for method call to block-body method without [Expressive]"); } [TestMethod] - public async Task ExtensionMethod_OnNonEnumReceiver_WarnsEXP0013() + public async Task ExtensionMethod_OnNonEnumReceiver_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -112,12 +112,12 @@ class C { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for non-enum extension method without [Expressive]"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for non-enum extension method without [Expressive]"); } [TestMethod] - public async Task PropertyWithoutExpressive_ReferencedInExpressive_WarnsEXP0013() + public async Task PropertyWithoutExpressive_ReferencedInExpressive_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -133,12 +133,12 @@ class Order { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for property without [Expressive] referenced in [Expressive] member"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for property without [Expressive] referenced in [Expressive] member"); } [TestMethod] - public async Task EXP0013_HasAdditionalLocation_PointingToDeclaration() + public async Task EXP0025_HasAdditionalLocation_PointingToDeclaration() { var diagnostics = await RunAnalyzerAsync( """ @@ -152,9 +152,9 @@ class C { } """); - var diag = diagnostics.First(d => d.Id == "EXP0013"); + var diag = diagnostics.First(d => d.Id == "EXP0025"); Assert.AreEqual(1, diag.AdditionalLocations.Count, - "EXP0013 should include the declaration as an additional location"); + "EXP0025 should include the declaration as an additional location"); } [TestMethod] @@ -173,7 +173,7 @@ class C { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn when referenced method already has [Expressive]"); } @@ -192,7 +192,7 @@ class C { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for auto-property access"); } @@ -211,7 +211,7 @@ class C { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for BCL method call"); } @@ -233,7 +233,7 @@ class C { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn when referenced property already has [Expressive]"); } @@ -262,12 +262,12 @@ class Order { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for enum extension method — generator expands these via TryEmitEnumMethodExpansion"); } [TestMethod] - public async Task ExpressiveQueryable_Select_WithNonExpressiveExtensionMethod_WarnsEXP0013() + public async Task ExpressiveQueryable_Select_WithNonExpressiveExtensionMethod_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -294,12 +294,12 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for non-[Expressive] extension method in IExpressiveQueryable Select lambda"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for non-[Expressive] extension method in IExpressiveQueryable Select lambda"); } [TestMethod] - public async Task ExpressiveQueryable_Where_WithNonExpressiveInstanceMethod_WarnsEXP0013() + public async Task ExpressiveQueryable_Where_WithNonExpressiveInstanceMethod_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -319,12 +319,12 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for non-[Expressive] instance method in IExpressiveQueryable Where lambda"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for non-[Expressive] instance method in IExpressiveQueryable Where lambda"); } [TestMethod] - public async Task ExpressiveQueryable_Select_WithNonExpressiveProperty_WarnsEXP0013() + public async Task ExpressiveQueryable_Select_WithNonExpressiveProperty_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -345,12 +345,12 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for non-[Expressive] property in IExpressiveQueryable Select lambda"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for non-[Expressive] property in IExpressiveQueryable Select lambda"); } [TestMethod] - public async Task ExpressiveQueryable_MethodGroup_WithNonExpressiveMethod_WarnsEXP0013() + public async Task ExpressiveQueryable_MethodGroup_WithNonExpressiveMethod_WarnsEXP0025() { var diagnostics = await RunAnalyzerAsync( """ @@ -379,8 +379,8 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"), - "Expected EXP0013 for non-[Expressive] method group in IExpressiveQueryable Select"); + Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0025"), + "Expected EXP0025 for non-[Expressive] method group in IExpressiveQueryable Select"); } [TestMethod] @@ -412,7 +412,7 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn when referenced method already has [Expressive] in LINQ lambda"); } @@ -436,7 +436,7 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for BCL method call in IExpressiveQueryable LINQ lambda"); } @@ -460,7 +460,7 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for auto-property access in IExpressiveQueryable LINQ lambda"); } @@ -494,13 +494,13 @@ void Run(IExpressiveQueryable source) { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for enum extension method in IExpressiveQueryable LINQ lambda"); } // When a stub on the same type registers an expression for a member via // [ExpressiveProperty("X")] or [ExpressiveFor(nameof(X))], the referenced member - // is effectively expressive — EXP0013 must not fire on it. + // is effectively expressive — EXP0025 must not fire on it. [TestMethod] public async Task PropertyAccess_ToExpressivePropertySynthesizedTarget_DoesNotWarn() @@ -521,7 +521,7 @@ partial class C { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for a reference to an [ExpressiveProperty]-synthesized target"); } @@ -545,7 +545,7 @@ class C { } """); - Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"), + Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0025"), "Should not warn for a reference to an [ExpressiveFor]-mapped same-type target"); } diff --git a/tests/ExpressiveSharp.IntegrationTests/Scenarios/Store/Models/LineItem.cs b/tests/ExpressiveSharp.IntegrationTests/Scenarios/Store/Models/LineItem.cs index 36d39eb..74ccdce 100644 --- a/tests/ExpressiveSharp.IntegrationTests/Scenarios/Store/Models/LineItem.cs +++ b/tests/ExpressiveSharp.IntegrationTests/Scenarios/Store/Models/LineItem.cs @@ -10,10 +10,10 @@ public class LineItem // Virtual [Expressive] member — regression coverage that static-type expansion still reaches // the query provider as translatable SQL. The reverted "bad commit" gate skipped expansion for - // virtual members, so this would hit EF Core untranslated and throw. EXP0038 is expected here + // virtual members, so this would hit EF Core untranslated and throw. EXP0024 is expected here // by design and suppressed. -#pragma warning disable EXP0038 +#pragma warning disable EXP0024 [Expressive] public virtual bool IsExpensive => UnitPrice > 40; -#pragma warning restore EXP0038 +#pragma warning restore EXP0024 } diff --git a/tests/ExpressiveSharp.IntegrationTests/Tests/ExpansionEdgeCasesTests.cs b/tests/ExpressiveSharp.IntegrationTests/Tests/ExpansionEdgeCasesTests.cs index a41aac8..d546160 100644 --- a/tests/ExpressiveSharp.IntegrationTests/Tests/ExpansionEdgeCasesTests.cs +++ b/tests/ExpressiveSharp.IntegrationTests/Tests/ExpansionEdgeCasesTests.cs @@ -124,7 +124,7 @@ public void Polyfill_StringRangeSlice_ProducesSubstring() } } -#pragma warning disable EXP0038 +#pragma warning disable EXP0024 public class VirtualDispatchBase { public int Id { get; set; } @@ -168,7 +168,7 @@ public class GreetDerived : GreetBase [Expressive] public override int Greet() => base.Greet() + 1; } -#pragma warning restore EXP0038 +#pragma warning restore EXP0024 public class RecursiveTree {