Moved the build to the .NET SDK v10.0.403 (#1156)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-04 11:48:11 +01:00
1 parent c0a75f39ac
commit 0375318b1b
10 files changed
+143 -153

No files matched your search

+1 -1
View File
@@ -793,7 +793,7 @@ jobs:
- name: Read the toolchain versions from the metadata - name: Read the toolchain versions from the metadata
run: | run: |
# The .NET SDK version. The format is '10.0.114 (commit 0427b9abbe)', # The .NET SDK version. The format is '10.0.403 (commit 5a2162bea6)',
# so we extract the version number alone: # so we extract the version number alone:
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/') dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
+1 -1
View File
@@ -28,7 +28,7 @@ Key structure:
## Important Development Notes ## Important Development Notes
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. - **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
- **Extension members** - Declare extension methods in an `extension` block of their static class, one block per receiver, never with a `this` parameter: MWAIS0016 refuses the classic form. The `<param>` of the receiver, and of a generic block its `<typeparam>`, belongs on the block, not on its members. The one exception is a method with a `params` parameter, which stays classic until the SDK in `metadata.txt` carries the fix for dotnet/roslyn#80024: for a member of an extension block, the compiler warns about every argument passed one by one to a `params` parameter (CS8620), see `JsRuntimeExtensions`. - **Extension members** - Declare extension methods in an `extension` block of their static class, one block per receiver, never with a `this` parameter: MWAIS0016 refuses the classic form. The `<param>` of the receiver, and of a generic block its `<typeparam>`, belongs on the block, not on its members.
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
- **Prompt injection reports** - The user hears once per action about what the filter removed, as soon as that action ends. Content which belongs to an action carries its scope from `PromptInjectionGuardService.BeginAction` down to where it is read or filtered; whatever is filtered without a scope is reported right away. Pass the scope as a parameter, never through a field or an `AsyncLocal`: the service serves the whole app, and the message bus delivers to other receivers in the execution context of whoever sends. - **Prompt injection reports** - The user hears once per action about what the filter removed, as soon as that action ends. Content which belongs to an action carries its scope from `PromptInjectionGuardService.BeginAction` down to where it is read or filtered; whatever is filtered without a scope is reported right away. Pass the scope as a parameter, never through a field or an `AsyncLocal`: the service serves the whole app, and the message bus delivers to other receivers in the execution context of whoever sends.
- **Paths in stored data** - A stored path to a file which AI Studio keeps below the data directory, such as a transcript or a tokenizer copy, is relative to that directory, so that a backup or a copy of the data directory works on another computer, under another user name, or on another OS. Mark such a property with `[JsonConverter(typeof(DataDirectoryRelativePathConverter))]`, for a positional record parameter with `[property: ...]`; in memory, the path stays absolute. Better still, derive the path from IDs and store none, as `VisualBriefingStore` does. Paths to files of the user stay absolute. Chats stored before v26.10.1 are migrated by a compatibility shim, see `documentation/compatibility-shims/2026-10-relative-data-paths.md`. - **Paths in stored data** - A stored path to a file which AI Studio keeps below the data directory, such as a transcript or a tokenizer copy, is relative to that directory, so that a backup or a copy of the data directory works on another computer, under another user name, or on another OS. Mark such a property with `[JsonConverter(typeof(DataDirectoryRelativePathConverter))]`, for a positional record parameter with `[property: ...]`; in memory, the path stays absolute. Better still, derive the path from IDs and store none, as `VisualBriefingStore` does. Paths to files of the user stay absolute. Chats stored before v26.10.1 are migrated by a compatibility shim, see `documentation/compatibility-shims/2026-10-relative-data-paths.md`.
@@ -3,16 +3,11 @@ using AIStudio.Tools.Services;
namespace AIStudio.Tools; namespace AIStudio.Tools;
//
// The methods with a params parameter stay classic extension methods instead of moving into the
// extension block: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns
// about every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Move
// them once the SDK in metadata.txt carries the fix; MWAIS0016 makes the same exception until then.
//
public static class JsRuntimeExtensions public static class JsRuntimeExtensions
{ {
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(JsRuntimeExtensions)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(JsRuntimeExtensions));
/// <param name="jsRuntime">The JS runtime to call.</param>
extension(IJSRuntime jsRuntime) extension(IJSRuntime jsRuntime)
{ {
public async Task GenerateAndShowDiff(string text1, string text2) public async Task GenerateAndShowDiff(string text1, string text2)
@@ -24,115 +19,115 @@ public static class JsRuntimeExtensions
{ {
await jsRuntime.InvokeVoidAsync("clearDiv", divId); await jsRuntime.InvokeVoidAsync("clearDiv", divId);
} }
}
/// <summary> /// <summary>
/// Calls a JavaScript function which returns nothing, and tolerates a circuit which is already gone. /// Calls a JavaScript function which returns nothing, and tolerates a circuit which is already gone.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Blazor cannot issue JS interop calls once the browser connection of a circuit is gone. That happens /// Blazor cannot issue JS interop calls once the browser connection of a circuit is gone. That happens
/// during every reload and while a component gets disposed, so the failure is expected rather than /// during every reload and while a component gets disposed, so the failure is expected rather than
/// exceptional. Discarding such a call is not an option, though: the discarded task keeps the fault /// exceptional. Discarding such a call is not an option, though: the discarded task keeps the fault
/// until the finalizer reports it as an unobserved task exception, without any hint at its origin. /// until the finalizer reports it as an unobserved task exception, without any hint at its origin.
/// This method is the one place which knows how to await such a call and what to do with its failure. /// This method is the one place which knows how to await such a call and what to do with its failure.
/// </remarks> /// </remarks>
/// <param name="jsRuntime">The JS runtime to call.</param> /// <param name="identifier">The name of the JavaScript function.</param>
/// <param name="identifier">The name of the JavaScript function.</param> /// <param name="args">The arguments for the JavaScript function.</param>
/// <param name="args">The arguments for the JavaScript function.</param> /// <returns>True when the browser ran the function. Callers which remember what they told the browser
/// <returns>True when the browser ran the function. Callers which remember what they told the browser /// must check this: a call which never arrived leaves the browser in its previous state.</returns>
/// must check this: a call which never arrived leaves the browser in its previous state.</returns> public async ValueTask<bool> TryInvokeVoidAsync(string identifier, params object?[]? args)
public static async ValueTask<bool> TryInvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, params object?[]? args)
{
try
{ {
await jsRuntime.InvokeVoidAsync(identifier, args); try
return true; {
await jsRuntime.InvokeVoidAsync(identifier, args);
return true;
}
catch (Exception exception)
{
LogInvocationFailure(exception, identifier);
return false;
}
} }
catch (Exception exception)
/// <summary>
/// Calls a JavaScript function which returns nothing, unless the circuit is known to be disconnected.
/// </summary>
/// <remarks>
/// Prefer this over the variant without a circuit state wherever the caller knows its circuit. While a
/// browser connection is gone, every single call would otherwise throw, which is needless work for
/// something we already know cannot succeed — a component of a disconnected circuit which keeps
/// rendering would produce one such exception per render.
/// </remarks>
/// <param name="circuitState">The circuit of the caller.</param>
/// <param name="identifier">The name of the JavaScript function.</param>
/// <param name="args">The arguments for the JavaScript function.</param>
/// <returns>True when the browser ran the function, false when it was skipped or failed.</returns>
public async ValueTask<bool> TryInvokeVoidAsync(CircuitStateService circuitState, string identifier, params object?[]? args)
{ {
LogInvocationFailure(exception, identifier); if (!circuitState.IsConnected)
return false; {
LOGGER.LogDebug("The JS call '{Identifier}' was skipped because the browser connection of the circuit '{CircuitId}' is down.", identifier, circuitState.CircuitId);
return false;
}
return await jsRuntime.TryInvokeVoidAsync(identifier, args);
}
/// <summary>
/// Calls a JavaScript function which returns a value, unless the circuit is known to be disconnected.
/// </summary>
/// <remarks>
/// The two parts of the result answer two different questions, and callers must keep them apart.
/// Whether the browser ran the function at all comes first: a call which never arrived says nothing
/// about the page, so nobody may act on an answer they did not get. What the function returned is the
/// second question, and there a null is a legitimate answer -- it means the browser looked and found
/// nothing.
/// </remarks>
/// <param name="circuitState">The circuit of the caller.</param>
/// <param name="identifier">The name of the JavaScript function.</param>
/// <param name="args">The arguments for the JavaScript function.</param>
/// <returns>Whether the browser ran the function, and what it returned.</returns>
public async ValueTask<(bool WasInvoked, TValue? Value)> TryInvokeAsync<TValue>(CircuitStateService circuitState, string identifier, params object?[]? args)
{
if (!circuitState.IsConnected)
{
LOGGER.LogDebug("The JS call '{Identifier}' was skipped because the browser connection of the circuit '{CircuitId}' is down.", identifier, circuitState.CircuitId);
return (false, default);
}
try
{
return (true, await jsRuntime.InvokeAsync<TValue>(identifier, args));
}
catch (Exception exception)
{
LogInvocationFailure(exception, identifier);
return (false, default);
}
} }
} }
/// <summary>
/// Calls a JavaScript function which returns nothing, unless the circuit is known to be disconnected.
/// </summary>
/// <remarks>
/// Prefer this over the variant without a circuit state wherever the caller knows its circuit. While a
/// browser connection is gone, every single call would otherwise throw, which is needless work for
/// something we already know cannot succeed — a component of a disconnected circuit which keeps
/// rendering would produce one such exception per render.
/// </remarks>
/// <param name="jsRuntime">The JS runtime to call.</param>
/// <param name="circuitState">The circuit of the caller.</param>
/// <param name="identifier">The name of the JavaScript function.</param>
/// <param name="args">The arguments for the JavaScript function.</param>
/// <returns>True when the browser ran the function, false when it was skipped or failed.</returns>
public static async ValueTask<bool> TryInvokeVoidAsync(this IJSRuntime jsRuntime, CircuitStateService circuitState, string identifier, params object?[]? args)
{
if (!circuitState.IsConnected)
{
LOGGER.LogDebug("The JS call '{Identifier}' was skipped because the browser connection of the circuit '{CircuitId}' is down.", identifier, circuitState.CircuitId);
return false;
}
return await jsRuntime.TryInvokeVoidAsync(identifier, args);
}
/// <summary>
/// Calls a JavaScript function which returns a value, unless the circuit is known to be disconnected.
/// </summary>
/// <remarks>
/// The two parts of the result answer two different questions, and callers must keep them apart.
/// Whether the browser ran the function at all comes first: a call which never arrived says nothing
/// about the page, so nobody may act on an answer they did not get. What the function returned is the
/// second question, and there a null is a legitimate answer -- it means the browser looked and found
/// nothing.
/// </remarks>
/// <param name="jsRuntime">The JS runtime to call.</param>
/// <param name="circuitState">The circuit of the caller.</param>
/// <param name="identifier">The name of the JavaScript function.</param>
/// <param name="args">The arguments for the JavaScript function.</param>
/// <returns>Whether the browser ran the function, and what it returned.</returns>
public static async ValueTask<(bool WasInvoked, TValue? Value)> TryInvokeAsync<TValue>(this IJSRuntime jsRuntime, CircuitStateService circuitState, string identifier, params object?[]? args)
{
if (!circuitState.IsConnected)
{
LOGGER.LogDebug("The JS call '{Identifier}' was skipped because the browser connection of the circuit '{CircuitId}' is down.", identifier, circuitState.CircuitId);
return (false, default);
}
try
{
return (true, await jsRuntime.InvokeAsync<TValue>(identifier, args));
}
catch (Exception exception)
{
LogInvocationFailure(exception, identifier);
return (false, default);
}
}
/// <summary>
/// Calls a function of a JavaScript module which returns nothing, and tolerates a circuit which is
/// already gone. See the remarks on the JS runtime variant of this method.
/// </summary>
/// <param name="module">The JavaScript module to call.</param> /// <param name="module">The JavaScript module to call.</param>
/// <param name="identifier">The name of the function inside the module.</param> extension(IJSObjectReference module)
/// <param name="args">The arguments for the function.</param>
/// <returns>True when the browser ran the function, false when it failed.</returns>
public static async ValueTask<bool> TryInvokeVoidAsync(this IJSObjectReference module, string identifier, params object?[]? args)
{ {
try /// <summary>
/// Calls a function of a JavaScript module which returns nothing, and tolerates a circuit which is
/// already gone. See the remarks on the JS runtime variant of this method.
/// </summary>
/// <param name="identifier">The name of the function inside the module.</param>
/// <param name="args">The arguments for the function.</param>
/// <returns>True when the browser ran the function, false when it failed.</returns>
public async ValueTask<bool> TryInvokeVoidAsync(string identifier, params object?[]? args)
{ {
await module.InvokeVoidAsync(identifier, args); try
return true; {
} await module.InvokeVoidAsync(identifier, args);
catch (Exception exception) return true;
{ }
LogInvocationFailure(exception, identifier); catch (Exception exception)
return false; {
LogInvocationFailure(exception, identifier);
return false;
}
} }
} }
@@ -49,32 +49,29 @@ internal static class MailToolArguments
private static readonly string[] SPECIAL_FOLDER_VALUES = [SPECIAL_FOLDER_SENT, SPECIAL_FOLDER_DRAFTS]; private static readonly string[] SPECIAL_FOLDER_VALUES = [SPECIAL_FOLDER_SENT, SPECIAL_FOLDER_DRAFTS];
/// <summary>
/// Adds the conditions to the arguments a mail tool describes.
/// </summary>
/// <param name="builder">The schema of the tool.</param> /// <param name="builder">The schema of the tool.</param>
/// <param name="toolAction">What the tool does with the mails, e.g., "search", completing "the mailboxes to ...".</param> extension(ToolParameterSchemaBuilder builder)
/// <param name="mailboxIds">The ids of the mailboxes the tool offers, or none while no mailboxes are known.</param> {
/// <returns>The schema, for further arguments.</returns> /// <summary>
/// <remarks> /// Adds the conditions to the arguments a mail tool describes.
/// A classic extension method instead of a member of an extension block: for those, the compiler of /// </summary>
/// the .NET 10.0.1xx SDK warns about every mailbox id passed one by one to the params parameter /// <param name="toolAction">What the tool does with the mails, e.g., "search", completing "the mailboxes to ...".</param>
/// (CS8620, dotnet/roslyn#80024). Move it once the SDK in metadata.txt carries the fix; MWAIS0016 makes /// <param name="mailboxIds">The ids of the mailboxes the tool offers, or none while no mailboxes are known.</param>
/// the same exception until then. /// <returns>The schema, for further arguments.</returns>
/// </remarks> public ToolParameterSchemaBuilder AddMailConditions(string toolAction, params string[] mailboxIds) => builder
public static ToolParameterSchemaBuilder AddMailConditions(this ToolParameterSchemaBuilder builder, string toolAction, params string[] mailboxIds) => builder .OptionalStringArray(MAILBOX_IDS_ARGUMENT, $"Optional IDs of the mailboxes to {toolAction}, out of those listed in the description of this tool. Leave it out to {toolAction} all of them.", mailboxIds)
.OptionalStringArray(MAILBOX_IDS_ARGUMENT, $"Optional IDs of the mailboxes to {toolAction}, out of those listed in the description of this tool. Leave it out to {toolAction} all of them.", mailboxIds) .OptionalString(FROM_ARGUMENT, $"Optional part of the address or the name of the sender, such as 'alice@example.org', 'example.org', or 'Alice'. At most {MAX_ADDRESS_CHARACTERS} characters.")
.OptionalString(FROM_ARGUMENT, $"Optional part of the address or the name of the sender, such as 'alice@example.org', 'example.org', or 'Alice'. At most {MAX_ADDRESS_CHARACTERS} characters.") .OptionalString(TO_ARGUMENT, $"Optional part of the address or the name of a recipient in To, Cc, or Bcc. At most {MAX_ADDRESS_CHARACTERS} characters.")
.OptionalString(TO_ARGUMENT, $"Optional part of the address or the name of a recipient in To, Cc, or Bcc. At most {MAX_ADDRESS_CHARACTERS} characters.") .OptionalString(AFTER_ARGUMENT, "Optional: only mails received at this point in time or later. A date such as 2026-09-01 stands for the beginning of that day in the time zone of the user. A date with a time of day such as 2026-09-01T14:30 is read in that time zone as well, unless it ends with an offset such as +02:00 or with Z.")
.OptionalString(AFTER_ARGUMENT, "Optional: only mails received at this point in time or later. A date such as 2026-09-01 stands for the beginning of that day in the time zone of the user. A date with a time of day such as 2026-09-01T14:30 is read in that time zone as well, unless it ends with an offset such as +02:00 or with Z.") .OptionalString(BEFORE_ARGUMENT, "Optional: only mails received before this point in time, in the same forms as the argument after. A date stands for the beginning of that day, so before 2026-09-30 leaves that day out.")
.OptionalString(BEFORE_ARGUMENT, "Optional: only mails received before this point in time, in the same forms as the argument after. A date stands for the beginning of that day, so before 2026-09-30 leaves that day out.") .OptionalBoolean(IS_UNREAD_ARGUMENT, "Optional: true for unread mails only, false for read ones only.")
.OptionalBoolean(IS_UNREAD_ARGUMENT, "Optional: true for unread mails only, false for read ones only.") .OptionalBoolean(IS_FLAGGED_ARGUMENT, "Optional: true for flagged mails only, false for unflagged ones only.")
.OptionalBoolean(IS_FLAGGED_ARGUMENT, "Optional: true for flagged mails only, false for unflagged ones only.") .OptionalBoolean(IS_ENCRYPTED_ARGUMENT, "Optional: true for encrypted mails only, false for unencrypted ones only. AI Studio cannot read the content of encrypted mails, only their header.")
.OptionalBoolean(IS_ENCRYPTED_ARGUMENT, "Optional: true for encrypted mails only, false for unencrypted ones only. AI Studio cannot read the content of encrypted mails, only their header.") .OptionalEnum(IMPORTANCE_ARGUMENT, "Optional importance the sender marked the mails with. Mails without such a mark count as normal.", IMPORTANCE_VALUES)
.OptionalEnum(IMPORTANCE_ARGUMENT, "Optional importance the sender marked the mails with. Mails without such a mark count as normal.", IMPORTANCE_VALUES) .OptionalBoolean(HAS_ATTACHMENTS_ARGUMENT, "Optional: true for mails with attachments only, false for mails without any.")
.OptionalBoolean(HAS_ATTACHMENTS_ARGUMENT, "Optional: true for mails with attachments only, false for mails without any.") .OptionalString(FOLDER_ARGUMENT, $"Optional full path of the folder the mails lie in, exactly as results show it, such as 'INBOX' or 'Archive/2026'. Subfolders are not included. For the sent mails or the drafts, use {SPECIAL_FOLDER_ARGUMENT} instead.")
.OptionalString(FOLDER_ARGUMENT, $"Optional full path of the folder the mails lie in, exactly as results show it, such as 'INBOX' or 'Archive/2026'. Subfolders are not included. For the sent mails or the drafts, use {SPECIAL_FOLDER_ARGUMENT} instead.") .OptionalEnum(SPECIAL_FOLDER_ARGUMENT, $"Optional: only the mails the user sent, or only the drafts of the user, however the server names these folders. Cannot be combined with {FOLDER_ARGUMENT}.", SPECIAL_FOLDER_VALUES);
.OptionalEnum(SPECIAL_FOLDER_ARGUMENT, $"Optional: only the mails the user sent, or only the drafts of the user, however the server names these folders. Cannot be combined with {FOLDER_ARGUMENT}.", SPECIAL_FOLDER_VALUES); }
/// <summary> /// <summary>
/// The value of the importance argument which stands for the given importance. /// The value of the importance argument which stands for the given importance.
@@ -20,10 +20,12 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<!-- <!--
Never above the Roslyn of the .NET SDK we build with (see metadata.txt): the 10.0.1xx SDKs ship Never above the Roslyn of the oldest .NET SDK which may build AI Studio: the 10.0.1xx SDKs ship
Roslyn 5.0. Built against a newer one, the compiler skips these analyzers with CS9057. Raise Roslyn 5.0. Built against a newer one, the compiler skips these analyzers with CS9057. The
both only together with the SDK feature band, with SourceGeneratedMappings, and with the Tests pipeline builds with the SDK in metadata.txt, a 10.0.4xx with Roslyn 5.9, but the .NET packages
project. of Linux distributions bring a 10.0.1xx, and so may the computer of a contributor. Raise both
only together with SourceGeneratedMappings and the Tests project, and only once a 10.0.1xx SDK
no longer has to build AI Studio.
--> -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
@@ -13,10 +13,7 @@ namespace SourceCodeRules.StyleAnalyzers;
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// AI Studio declares its extension members in extension blocks, one per receiver, so that all members of a /// AI Studio declares its extension members in extension blocks, one per receiver, so that all members of a
/// receiver read alike and extension properties can join them. A method with a params parameter stays a /// receiver read alike and extension properties can join them.
/// classic one for now: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns about
/// every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Drop that exception
/// once the SDK in metadata.txt carries the fix.
/// </remarks> /// </remarks>
#pragma warning disable RS1038 #pragma warning disable RS1038
[DiagnosticAnalyzer(LanguageNames.CSharp)] [DiagnosticAnalyzer(LanguageNames.CSharp)]
@@ -29,7 +26,7 @@ public sealed class ClassicExtensionMethodAnalyzer : DiagnosticAnalyzer
private const string MESSAGE_FORMAT = "Declare '{0}' as a member of an extension block for its receiver instead of with a this parameter"; private const string MESSAGE_FORMAT = "Declare '{0}' as a member of an extension block for its receiver instead of with a this parameter";
private const string DESCRIPTION = "AI Studio declares its extension members in extension blocks, one per receiver. Only a method with a params parameter stays a classic extension method, until the SDK fixes dotnet/roslyn#80024."; private const string DESCRIPTION = "AI Studio declares its extension members in extension blocks, one per receiver.";
private const string CATEGORY = "Style"; private const string CATEGORY = "Style";
@@ -55,9 +52,6 @@ public sealed class ClassicExtensionMethodAnalyzer : DiagnosticAnalyzer
if (thisKeyword.IsKind(SyntaxKind.None)) if (thisKeyword.IsKind(SyntaxKind.None))
return; return;
if (parameters.Any(parameter => parameter.Modifiers.Any(SyntaxKind.ParamsKeyword)))
return;
context.ReportDiagnostic(Diagnostic.Create(RULE, thisKeyword.GetLocation(), method.Identifier.Text)); context.ReportDiagnostic(Diagnostic.Create(RULE, thisKeyword.GetLocation(), method.Identifier.Text));
} }
} }
@@ -21,10 +21,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<!-- <!--
Never above the Roslyn of the .NET SDK we build with (see metadata.txt): the 10.0.1xx SDKs ship Never above the Roslyn of the oldest .NET SDK which may build AI Studio: the 10.0.1xx SDKs ship
Roslyn 5.0. Built against a newer one, the compiler skips this generator with CS9057, and the Roslyn 5.0. Built against a newer one, the compiler skips this generator with CS9057, and the
generated namespaces such as AIStudio.Tools.PluginSystem.Assistants.Icons go missing. Raise it generated namespaces such as AIStudio.Tools.PluginSystem.Assistants.Icons go missing. The
only together with the SDK feature band, with SourceCodeRules, and with the Tests project. pipeline builds with the SDK in metadata.txt, a 10.0.4xx with Roslyn 5.9, but the .NET packages
of Linux distributions bring a 10.0.1xx, and so may the computer of a contributor. Raise it only
together with SourceCodeRules and the Tests project, and only once a 10.0.1xx SDK no longer has
to build AI Studio.
--> -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
</ItemGroup> </ItemGroup>
@@ -11,9 +11,7 @@ namespace AIStudio.Tests.Analyzers;
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// AI Studio declares all its extension members in extension blocks, one per receiver. MWAIS0016 refuses the /// AI Studio declares all its extension members in extension blocks, one per receiver. MWAIS0016 refuses the
/// classic form while compiling, except for a method with a params parameter: for a member of an extension /// classic form while compiling.
/// block, the compiler of the .NET 10.0.1xx SDK warns about every argument passed one by one to a params
/// parameter (CS8620, dotnet/roslyn#80024).
/// </remarks> /// </remarks>
[TestFixture] [TestFixture]
public sealed class ClassicExtensionMethodAnalyzerTests public sealed class ClassicExtensionMethodAnalyzerTests
@@ -24,6 +22,7 @@ public sealed class ClassicExtensionMethodAnalyzerTests
[TestCase("public static T Echo<T>(this T value) where T : class => value;", "Echo", TestName = "A generic extension method")] [TestCase("public static T Echo<T>(this T value) where T : class => value;", "Echo", TestName = "A generic extension method")]
[TestCase("public static void Bump(this ref int value) => value++;", "Bump", TestName = "An extension method with a ref receiver")] [TestCase("public static void Bump(this ref int value) => value++;", "Bump", TestName = "An extension method with a ref receiver")]
[TestCase("public static System.Threading.Tasks.Task<string> LabelAsync(this Meeting meeting) => System.Threading.Tasks.Task.FromResult(meeting.Name);", "LabelAsync", TestName = "An asynchronous extension method")] [TestCase("public static System.Threading.Tasks.Task<string> LabelAsync(this Meeting meeting) => System.Threading.Tasks.Task.FromResult(meeting.Name);", "LabelAsync", TestName = "An asynchronous extension method")]
[TestCase("public static string Label(this Meeting meeting, params string[] prefixes) => string.Concat(prefixes) + meeting.Name;", "Label", TestName = "An extension method with a params parameter")]
public async Task AClassicExtensionMethodIsRefused(string member, string name) public async Task AClassicExtensionMethodIsRefused(string member, string name)
{ {
var reported = await AnalyzeAsync(member); var reported = await AnalyzeAsync(member);
@@ -39,7 +38,6 @@ public sealed class ClassicExtensionMethodAnalyzerTests
[TestCase("extension(Meeting meeting) { public string Label() => meeting.Name; }", TestName = "A member of an extension block")] [TestCase("extension(Meeting meeting) { public string Label() => meeting.Name; }", TestName = "A member of an extension block")]
[TestCase("extension<T>(T value) where T : class { public T Echo() => value; }", TestName = "A member of a generic extension block")] [TestCase("extension<T>(T value) where T : class { public T Echo() => value; }", TestName = "A member of a generic extension block")]
[TestCase("public static string Label(Meeting meeting) => meeting.Name;", TestName = "A static method without a this parameter")] [TestCase("public static string Label(Meeting meeting) => meeting.Name;", TestName = "A static method without a this parameter")]
[TestCase("public static string Label(this Meeting meeting, params string[] prefixes) => string.Concat(prefixes) + meeting.Name;", TestName = "An extension method with a params parameter")]
public async Task AnythingElseIsLeftAlone(string member) public async Task AnythingElseIsLeftAlone(string member)
{ {
var reported = await AnalyzeAsync(member); var reported = await AnalyzeAsync(member);
+2 -1
View File
@@ -14,7 +14,8 @@
<!-- <!--
Held at the version the two analyzer projects build against, so that a test compiling a Held at the version the two analyzer projects build against, so that a test compiling a
snippet uses the same Roslyn the analyzers themselves were written for. Raise all three snippet uses the same Roslyn the analyzers themselves were written for. Raise all three
together, and never above the Roslyn of the .NET SDK in metadata.txt (5.0 for 10.0.1xx). together, and never above the Roslyn of the oldest .NET SDK which may build AI Studio (5.0 for
10.0.1xx), even though the SDK in metadata.txt ships a newer one; SourceCodeRules explains why.
--> -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
+1 -1
View File
@@ -1,7 +1,7 @@
26.10.1 26.10.1
2026-10-29 07:39:07 UTC 2026-10-29 07:39:07 UTC
259 259
10.0.114 (commit 0427b9abbe) 10.0.403 (commit 5a2162bea6)
10.0.14 (commit 0427b9abbe) 10.0.14 (commit 0427b9abbe)
1.99.1 (commit 58cf8f9c3) 1.99.1 (commit 58cf8f9c3)
9.12.0 9.12.0